Import kdepim-runtime_16.04.2.orig.tar.xz
authorMaximiliano Curia <maxy@debian.org>
Tue, 28 Jun 2016 17:47:52 +0000 (18:47 +0100)
committerMaximiliano Curia <maxy@debian.org>
Tue, 28 Jun 2016 17:47:52 +0000 (18:47 +0100)
[dgit import orig kdepim-runtime_16.04.2.orig.tar.xz]

970 files changed:
.arcconfig [new file with mode: 0644]
.emacs-dirvars [new file with mode: 0644]
.gitignore [new file with mode: 0644]
.krazy [new file with mode: 0644]
.reviewboardrc [new file with mode: 0644]
CMakeLists.txt [new file with mode: 0644]
COPYING [new file with mode: 0644]
COPYING.LIB [new file with mode: 0644]
CTestConfig.cmake [new file with mode: 0644]
CTestCustom.cmake [new file with mode: 0644]
MIGRATE-CONFIG-APPS [new file with mode: 0644]
Mainpage.dox [new file with mode: 0644]
README [new file with mode: 0644]
agents/.krazy [new file with mode: 0644]
agents/CMakeLists.txt [new file with mode: 0644]
agents/Info.plist.template [new file with mode: 0644]
agents/Mainpage.dox [new file with mode: 0644]
agents/cmake/FindXsltproc.cmake [new file with mode: 0644]
agents/invitations/CMakeLists.txt [new file with mode: 0644]
agents/invitations/Messages.sh [new file with mode: 0755]
agents/invitations/incidenceattribute.cpp [new file with mode: 0644]
agents/invitations/incidenceattribute.h [new file with mode: 0644]
agents/invitations/invitationsagent.cpp [new file with mode: 0644]
agents/invitations/invitationsagent.desktop [new file with mode: 0644]
agents/invitations/invitationsagent.h [new file with mode: 0644]
agents/maildispatcher/CMakeLists.txt [new file with mode: 0644]
agents/maildispatcher/Messages.sh [new file with mode: 0755]
agents/maildispatcher/TODO [new file with mode: 0644]
agents/maildispatcher/akonadi_maildispatcher_agent.notifyrc [new file with mode: 0644]
agents/maildispatcher/autotests/CMakeLists.txt [new file with mode: 0644]
agents/maildispatcher/autotests/TODO [new file with mode: 0644]
agents/maildispatcher/autotests/aborttest.cpp [new file with mode: 0644]
agents/maildispatcher/autotests/aborttest.h [new file with mode: 0644]
agents/maildispatcher/autotests/dupetest.cpp [new file with mode: 0644]
agents/maildispatcher/autotests/dupetest.h [new file with mode: 0644]
agents/maildispatcher/autotests/unittestenv/config.xml [new file with mode: 0644]
agents/maildispatcher/autotests/unittestenv/kdehome/share/config/akonadi-firstrunrc [new file with mode: 0644]
agents/maildispatcher/autotests/unittestenv/kdehome/share/config/akonadi_knut_resource_0rc [new file with mode: 0644]
agents/maildispatcher/autotests/unittestenv/kdehome/share/config/akonadi_mailtransport_dummy_resource_0rc [new file with mode: 0644]
agents/maildispatcher/autotests/unittestenv/kdehome/share/config/kdebugrc [new file with mode: 0644]
agents/maildispatcher/autotests/unittestenv/kdehome/share/config/kdedrc [new file with mode: 0644]
agents/maildispatcher/autotests/unittestenv/kdehome/share/config/kwalletrc [new file with mode: 0644]
agents/maildispatcher/autotests/unittestenv/kdehome/share/config/mailtransports [new file with mode: 0644]
agents/maildispatcher/autotests/unittestenv/kdehome/share/config/qttestrc [new file with mode: 0644]
agents/maildispatcher/autotests/unittestenv/kdehome/testdata.xml [new file with mode: 0644]
agents/maildispatcher/autotests/unittestenv/xdgconfig/akonadi/akonadiserverrc [new file with mode: 0644]
agents/maildispatcher/autotests/unittestenv/xdglocal/.keep [new file with mode: 0644]
agents/maildispatcher/maildispatcheragent.cpp [new file with mode: 0644]
agents/maildispatcher/maildispatcheragent.desktop [new file with mode: 0644]
agents/maildispatcher/maildispatcheragent.h [new file with mode: 0644]
agents/maildispatcher/maildispatcheragent.kcfg [new file with mode: 0644]
agents/maildispatcher/org.freedesktop.Akonadi.MailDispatcherAgent.xml [new file with mode: 0644]
agents/maildispatcher/outboxqueue.cpp [new file with mode: 0644]
agents/maildispatcher/outboxqueue.h [new file with mode: 0644]
agents/maildispatcher/sendjob.cpp [new file with mode: 0644]
agents/maildispatcher/sendjob.h [new file with mode: 0644]
agents/maildispatcher/sentactionhandler.cpp [new file with mode: 0644]
agents/maildispatcher/sentactionhandler.h [new file with mode: 0644]
agents/maildispatcher/settings.kcfgc [new file with mode: 0644]
agents/maildispatcher/settings.ui [new file with mode: 0644]
agents/maildispatcher/storeresultjob.cpp [new file with mode: 0644]
agents/maildispatcher/storeresultjob.h [new file with mode: 0644]
agents/migration/CMakeLists.txt [new file with mode: 0644]
agents/migration/Messages.sh [new file with mode: 0755]
agents/migration/autotests/CMakeLists.txt [new file with mode: 0644]
agents/migration/autotests/dummymigrator.cpp [new file with mode: 0644]
agents/migration/autotests/dummymigrator.h [new file with mode: 0644]
agents/migration/autotests/schedulertest.cpp [new file with mode: 0644]
agents/migration/migrationagent.cpp [new file with mode: 0644]
agents/migration/migrationagent.desktop [new file with mode: 0644]
agents/migration/migrationagent.h [new file with mode: 0644]
agents/migration/migrationexecutor.cpp [new file with mode: 0644]
agents/migration/migrationexecutor.h [new file with mode: 0644]
agents/migration/migrationscheduler.cpp [new file with mode: 0644]
agents/migration/migrationscheduler.h [new file with mode: 0644]
agents/migration/migrationstatuswidget.cpp [new file with mode: 0644]
agents/migration/migrationstatuswidget.h [new file with mode: 0644]
agents/newmailnotifier/CMakeLists.txt [new file with mode: 0644]
agents/newmailnotifier/Messages.sh [new file with mode: 0755]
agents/newmailnotifier/TODO [new file with mode: 0644]
agents/newmailnotifier/akonadi_newmailnotifier_agent.notifyrc [new file with mode: 0644]
agents/newmailnotifier/newmailnotifieragent.cpp [new file with mode: 0644]
agents/newmailnotifier/newmailnotifieragent.desktop [new file with mode: 0644]
agents/newmailnotifier/newmailnotifieragent.h [new file with mode: 0644]
agents/newmailnotifier/newmailnotifieragentsettings.kcfg [new file with mode: 0644]
agents/newmailnotifier/newmailnotifieragentsettings.kcfgc [new file with mode: 0644]
agents/newmailnotifier/newmailnotifierselectcollectionwidget.cpp [new file with mode: 0644]
agents/newmailnotifier/newmailnotifierselectcollectionwidget.h [new file with mode: 0644]
agents/newmailnotifier/newmailnotifiersettingsdialog.cpp [new file with mode: 0644]
agents/newmailnotifier/newmailnotifiersettingsdialog.h [new file with mode: 0644]
agents/newmailnotifier/newmailnotifiershowmessagejob.cpp [new file with mode: 0644]
agents/newmailnotifier/newmailnotifiershowmessagejob.h [new file with mode: 0644]
agents/newmailnotifier/org.freedesktop.Akonadi.NewMailNotifier.xml [new file with mode: 0644]
agents/newmailnotifier/specialnotifierjob.cpp [new file with mode: 0644]
agents/newmailnotifier/specialnotifierjob.h [new file with mode: 0644]
akonadi-prefix.h.cmake [new file with mode: 0644]
akonadi-version.h.cmake [new file with mode: 0644]
cmake/modules/FindXsltproc.cmake [new file with mode: 0644]
defaultsetup/CMakeLists.txt [new file with mode: 0644]
defaultsetup/birthdaycalendar.desktop [new file with mode: 0644]
defaultsetup/defaultaddressbook.desktop [new file with mode: 0644]
defaultsetup/defaultcalendar.desktop [new file with mode: 0644]
defaultsetup/defaultnotebook.desktop [new file with mode: 0644]
doc/git-migration.txt [new file with mode: 0644]
doc/libakonadi.xmi [new file with mode: 0644]
doc/pics/akonadi_agent_handling.eps [new file with mode: 0644]
doc/pics/akonadi_agent_handling.png [new file with mode: 0644]
doc/pics/akonadi_agent_handling_small.png [new file with mode: 0644]
doc/pics/akonadi_client_search.eps [new file with mode: 0644]
doc/pics/akonadi_client_search.png [new file with mode: 0644]
doc/pics/akonadi_client_search_small.png [new file with mode: 0644]
doc/pics/akonadi_communication.xmi [new file with mode: 0644]
doc/pics/akonadi_concept_schema.sla [new file with mode: 0644]
doc/pics/akonadi_overview_uml.png [new file with mode: 0644]
doc/pics/akonadi_overview_uml.ps [new file with mode: 0644]
doc/pics/akonadi_overview_uml_small.png [new file with mode: 0644]
doc/pics/concept.eps [new file with mode: 0644]
doc/pics/concept.png [new file with mode: 0644]
doc/pics/concept.sla [new file with mode: 0644]
doc/pics/convert.sh [new file with mode: 0755]
doc/todo.dox [new file with mode: 0644]
kdepim-mime.xml [new file with mode: 0644]
kdepim-runtime-version.h.cmake [new file with mode: 0644]
kdepim-runtime.categories [new file with mode: 0644]
kioslave/CMakeLists.txt [new file with mode: 0644]
kioslave/Messages.sh [new file with mode: 0644]
kioslave/akonadi.protocol [new file with mode: 0644]
kioslave/akonadislave.cpp [new file with mode: 0644]
kioslave/akonadislave.h [new file with mode: 0644]
migration/CMakeLists.txt [new file with mode: 0644]
migration/cmake/FindXsltproc.cmake [new file with mode: 0644]
migration/entitytreecreatejob.cpp [new file with mode: 0644]
migration/entitytreecreatejob.h [new file with mode: 0644]
migration/gid/CMakeLists.txt [new file with mode: 0644]
migration/gid/Messages.sh [new file with mode: 0644]
migration/gid/gidmigrationjob.cpp [new file with mode: 0644]
migration/gid/gidmigrationjob.h [new file with mode: 0644]
migration/gid/gidmigrator.cpp [new file with mode: 0644]
migration/gid/gidmigrator.h [new file with mode: 0644]
migration/gid/main.cpp [new file with mode: 0644]
migration/infodialog.cpp [new file with mode: 0644]
migration/infodialog.h [new file with mode: 0644]
migration/kmigratorbase.cpp [new file with mode: 0644]
migration/kmigratorbase.h [new file with mode: 0644]
migration/migratorbase.cpp [new file with mode: 0644]
migration/migratorbase.h [new file with mode: 0644]
plugins/CMakeLists.txt [new file with mode: 0644]
plugins/Messages.sh [new file with mode: 0644]
plugins/akonadi_serializer_addressee.cpp [new file with mode: 0644]
plugins/akonadi_serializer_addressee.desktop [new file with mode: 0644]
plugins/akonadi_serializer_addressee.h [new file with mode: 0644]
plugins/akonadi_serializer_contactgroup.cpp [new file with mode: 0644]
plugins/akonadi_serializer_contactgroup.desktop [new file with mode: 0644]
plugins/akonadi_serializer_contactgroup.h [new file with mode: 0644]
plugins/akonadi_serializer_kalarm.cpp [new file with mode: 0644]
plugins/akonadi_serializer_kalarm.desktop [new file with mode: 0644]
plugins/akonadi_serializer_kalarm.h [new file with mode: 0644]
plugins/akonadi_serializer_kcalcore.cpp [new file with mode: 0644]
plugins/akonadi_serializer_kcalcore.desktop [new file with mode: 0644]
plugins/akonadi_serializer_kcalcore.h [new file with mode: 0644]
plugins/akonadi_serializer_mail.cpp [new file with mode: 0644]
plugins/akonadi_serializer_mail.desktop [new file with mode: 0644]
plugins/akonadi_serializer_mail.h [new file with mode: 0644]
plugins/autotests/CMakeLists.txt [new file with mode: 0644]
plugins/autotests/addresseeserializertest.cpp [new file with mode: 0644]
plugins/autotests/kcalcoreserializertest.cpp [new file with mode: 0644]
plugins/autotests/mailserializerplugintest.cpp [new file with mode: 0644]
plugins/autotests/mailserializerplugintest.h [new file with mode: 0644]
plugins/autotests/mailserializertest.cpp [new file with mode: 0644]
plugins/autotests/mailserializertest.h [new file with mode: 0644]
plugins/kaeventformatter.cpp [new file with mode: 0644]
plugins/kaeventformatter.h [new file with mode: 0644]
resources/.krazy [new file with mode: 0644]
resources/CMakeLists.txt [new file with mode: 0644]
resources/Info.plist.template [new file with mode: 0644]
resources/Mainpage.dox [new file with mode: 0644]
resources/akonotes/CMakeLists.txt [new file with mode: 0644]
resources/akonotes/Messages.sh [new file with mode: 0644]
resources/akonotes/akonotesresource.cpp [new file with mode: 0644]
resources/akonotes/akonotesresource.desktop [new file with mode: 0644]
resources/akonotes/akonotesresource.h [new file with mode: 0644]
resources/birthdays/CMakeLists.txt [new file with mode: 0644]
resources/birthdays/Messages.sh [new file with mode: 0644]
resources/birthdays/birthdaysresource.cpp [new file with mode: 0644]
resources/birthdays/birthdaysresource.desktop [new file with mode: 0644]
resources/birthdays/birthdaysresource.h [new file with mode: 0644]
resources/birthdays/birthdaysresource.kcfg [new file with mode: 0644]
resources/birthdays/configdialog.cpp [new file with mode: 0644]
resources/birthdays/configdialog.h [new file with mode: 0644]
resources/birthdays/configdialog.ui [new file with mode: 0644]
resources/birthdays/settings.kcfgc [new file with mode: 0644]
resources/cmake/FindXsltproc.cmake [new file with mode: 0644]
resources/contacts/CMakeLists.txt [new file with mode: 0644]
resources/contacts/Messages.sh [new file with mode: 0644]
resources/contacts/contactsresource.cpp [new file with mode: 0644]
resources/contacts/contactsresource.desktop [new file with mode: 0644]
resources/contacts/contactsresource.h [new file with mode: 0644]
resources/contacts/contactsresource.kcfg [new file with mode: 0644]
resources/contacts/settings.kcfgc [new file with mode: 0644]
resources/contacts/settingsdialog.cpp [new file with mode: 0644]
resources/contacts/settingsdialog.h [new file with mode: 0644]
resources/contacts/settingsdialog.ui [new file with mode: 0644]
resources/contacts/wizard/CMakeLists.txt [new file with mode: 0644]
resources/contacts/wizard/Messages.sh [new file with mode: 0644]
resources/contacts/wizard/contactswizard.desktop [new file with mode: 0644]
resources/contacts/wizard/contactswizard.es.cmake [new file with mode: 0644]
resources/contacts/wizard/contactswizard.ui [new file with mode: 0644]
resources/dav/CMakeLists.txt [new file with mode: 0644]
resources/dav/COPYING [new file with mode: 0644]
resources/dav/README [new file with mode: 0644]
resources/dav/TODO [new file with mode: 0644]
resources/dav/common/davcollection.cpp [new file with mode: 0644]
resources/dav/common/davcollection.h [new file with mode: 0644]
resources/dav/common/davcollectiondeletejob.cpp [new file with mode: 0644]
resources/dav/common/davcollectiondeletejob.h [new file with mode: 0644]
resources/dav/common/davcollectionmodifyjob.cpp [new file with mode: 0644]
resources/dav/common/davcollectionmodifyjob.h [new file with mode: 0644]
resources/dav/common/davcollectionsfetchjob.cpp [new file with mode: 0644]
resources/dav/common/davcollectionsfetchjob.h [new file with mode: 0644]
resources/dav/common/davcollectionsmultifetchjob.cpp [new file with mode: 0644]
resources/dav/common/davcollectionsmultifetchjob.h [new file with mode: 0644]
resources/dav/common/davitem.cpp [new file with mode: 0644]
resources/dav/common/davitem.h [new file with mode: 0644]
resources/dav/common/davitemcreatejob.cpp [new file with mode: 0644]
resources/dav/common/davitemcreatejob.h [new file with mode: 0644]
resources/dav/common/davitemdeletejob.cpp [new file with mode: 0644]
resources/dav/common/davitemdeletejob.h [new file with mode: 0644]
resources/dav/common/davitemfetchjob.cpp [new file with mode: 0644]
resources/dav/common/davitemfetchjob.h [new file with mode: 0644]
resources/dav/common/davitemmodifyjob.cpp [new file with mode: 0644]
resources/dav/common/davitemmodifyjob.h [new file with mode: 0644]
resources/dav/common/davitemsfetchjob.cpp [new file with mode: 0644]
resources/dav/common/davitemsfetchjob.h [new file with mode: 0644]
resources/dav/common/davitemslistjob.cpp [new file with mode: 0644]
resources/dav/common/davitemslistjob.h [new file with mode: 0644]
resources/dav/common/davjobbase.cpp [new file with mode: 0644]
resources/dav/common/davjobbase.h [new file with mode: 0644]
resources/dav/common/davmanager.cpp [new file with mode: 0644]
resources/dav/common/davmanager.h [new file with mode: 0644]
resources/dav/common/davmultigetprotocol.cpp [new file with mode: 0644]
resources/dav/common/davmultigetprotocol.h [new file with mode: 0644]
resources/dav/common/davprincipalhomesetsfetchjob.cpp [new file with mode: 0644]
resources/dav/common/davprincipalhomesetsfetchjob.h [new file with mode: 0644]
resources/dav/common/davprincipalsearchjob.cpp [new file with mode: 0644]
resources/dav/common/davprincipalsearchjob.h [new file with mode: 0644]
resources/dav/common/davprotocolbase.cpp [new file with mode: 0644]
resources/dav/common/davprotocolbase.h [new file with mode: 0644]
resources/dav/common/davutils.cpp [new file with mode: 0644]
resources/dav/common/davutils.h [new file with mode: 0644]
resources/dav/common/etagcache.cpp [new file with mode: 0644]
resources/dav/common/etagcache.h [new file with mode: 0644]
resources/dav/protocols/caldavprotocol.cpp [new file with mode: 0644]
resources/dav/protocols/caldavprotocol.h [new file with mode: 0644]
resources/dav/protocols/carddavprotocol.cpp [new file with mode: 0644]
resources/dav/protocols/carddavprotocol.h [new file with mode: 0644]
resources/dav/protocols/groupdavprotocol.cpp [new file with mode: 0644]
resources/dav/protocols/groupdavprotocol.h [new file with mode: 0644]
resources/dav/resource/CMakeLists.txt [new file with mode: 0644]
resources/dav/resource/Messages.sh [new file with mode: 0644]
resources/dav/resource/akonadi-resources.png [new file with mode: 0644]
resources/dav/resource/configdialog.cpp [new file with mode: 0644]
resources/dav/resource/configdialog.h [new file with mode: 0644]
resources/dav/resource/configdialog.ui [new file with mode: 0644]
resources/dav/resource/ctagattribute.cpp [new file with mode: 0644]
resources/dav/resource/ctagattribute.h [new file with mode: 0644]
resources/dav/resource/davfreebusyhandler.cpp [new file with mode: 0644]
resources/dav/resource/davfreebusyhandler.h [new file with mode: 0644]
resources/dav/resource/davgroupwareprovider.desktop [new file with mode: 0644]
resources/dav/resource/davgroupwareresource.cpp [new file with mode: 0644]
resources/dav/resource/davgroupwareresource.desktop [new file with mode: 0644]
resources/dav/resource/davgroupwareresource.h [new file with mode: 0644]
resources/dav/resource/davgroupwareresource.kcfg [new file with mode: 0644]
resources/dav/resource/davprotocolattribute.cpp [new file with mode: 0644]
resources/dav/resource/davprotocolattribute.h [new file with mode: 0644]
resources/dav/resource/searchdialog.cpp [new file with mode: 0644]
resources/dav/resource/searchdialog.h [new file with mode: 0644]
resources/dav/resource/searchdialog.ui [new file with mode: 0644]
resources/dav/resource/settings.cpp [new file with mode: 0644]
resources/dav/resource/settings.h [new file with mode: 0644]
resources/dav/resource/settingsbase.kcfgc [new file with mode: 0644]
resources/dav/resource/setupwizard.cpp [new file with mode: 0644]
resources/dav/resource/setupwizard.h [new file with mode: 0644]
resources/dav/resource/urlconfigurationdialog.cpp [new file with mode: 0644]
resources/dav/resource/urlconfigurationdialog.h [new file with mode: 0644]
resources/dav/resource/urlconfigurationdialog.ui [new file with mode: 0644]
resources/dav/services/citadel.desktop [new file with mode: 0644]
resources/dav/services/davical.desktop [new file with mode: 0644]
resources/dav/services/egroupware.desktop [new file with mode: 0644]
resources/dav/services/opengroupware.desktop [new file with mode: 0644]
resources/dav/services/owncloud-pre5.desktop [new file with mode: 0644]
resources/dav/services/owncloud.desktop [new file with mode: 0644]
resources/dav/services/scalix.desktop [new file with mode: 0644]
resources/dav/services/sogo.desktop [new file with mode: 0644]
resources/dav/services/yahoo.desktop [new file with mode: 0644]
resources/dav/services/zarafa.desktop [new file with mode: 0644]
resources/dav/services/zimbra.desktop [new file with mode: 0644]
resources/folderarchivesettings/CMakeLists.txt [new file with mode: 0644]
resources/folderarchivesettings/Messages.sh [new file with mode: 0755]
resources/folderarchivesettings/autotests/CMakeLists.txt [new file with mode: 0644]
resources/folderarchivesettings/autotests/folderarchiveaccountinfotest.cpp [new file with mode: 0644]
resources/folderarchivesettings/autotests/folderarchiveaccountinfotest.h [new file with mode: 0644]
resources/folderarchivesettings/folderarchiveaccountinfo.cpp [new file with mode: 0644]
resources/folderarchivesettings/folderarchiveaccountinfo.h [new file with mode: 0644]
resources/folderarchivesettings/folderarchivesettingpage.cpp [new file with mode: 0644]
resources/folderarchivesettings/folderarchivesettingpage.h [new file with mode: 0644]
resources/folderarchivesettings/folderarchiveutil.cpp [new file with mode: 0644]
resources/folderarchivesettings/folderarchiveutil.h [new file with mode: 0644]
resources/gmail/CMakeLists.txt [new file with mode: 0644]
resources/gmail/Messages.sh [new file with mode: 0755]
resources/gmail/gmailchangeitemslabelstask.cpp [new file with mode: 0644]
resources/gmail/gmailchangeitemslabelstask.h [new file with mode: 0644]
resources/gmail/gmailconfigdialog.cpp [new file with mode: 0644]
resources/gmail/gmailconfigdialog.h [new file with mode: 0644]
resources/gmail/gmailconfigdialog.ui [new file with mode: 0644]
resources/gmail/gmaillabelattribute.cpp [new file with mode: 0644]
resources/gmail/gmaillabelattribute.h [new file with mode: 0644]
resources/gmail/gmaillinkitemstask.cpp [new file with mode: 0644]
resources/gmail/gmaillinkitemstask.h [new file with mode: 0644]
resources/gmail/gmailmessagehelper.cpp [new file with mode: 0644]
resources/gmail/gmailmessagehelper.h [new file with mode: 0644]
resources/gmail/gmailpasswordrequester.cpp [new file with mode: 0644]
resources/gmail/gmailpasswordrequester.h [new file with mode: 0644]
resources/gmail/gmailresource.cpp [new file with mode: 0644]
resources/gmail/gmailresource.desktop [new file with mode: 0644]
resources/gmail/gmailresource.h [new file with mode: 0644]
resources/gmail/gmailresource.kcfg [new file with mode: 0644]
resources/gmail/gmailresourcestate.cpp [new file with mode: 0644]
resources/gmail/gmailresourcestate.h [new file with mode: 0644]
resources/gmail/gmailretrievecollectionstask.cpp [new file with mode: 0644]
resources/gmail/gmailretrievecollectionstask.h [new file with mode: 0644]
resources/gmail/gmailretrieveitemstask.cpp [new file with mode: 0644]
resources/gmail/gmailretrieveitemstask.h [new file with mode: 0644]
resources/gmail/gmailsettings.cpp [new file with mode: 0644]
resources/gmail/gmailsettings.h [new file with mode: 0644]
resources/gmail/saslplugin/CMakeLists.txt [new file with mode: 0644]
resources/gmail/saslplugin/config.h [new file with mode: 0644]
resources/gmail/saslplugin/plugin_common.c [new file with mode: 0644]
resources/gmail/saslplugin/plugin_common.h [new file with mode: 0644]
resources/gmail/saslplugin/xoauth2plugin.c [new file with mode: 0644]
resources/gmail/saslplugin/xoauth2plugin_init.c [new file with mode: 0644]
resources/gmail/settingsbase.kcfgc [new file with mode: 0644]
resources/google/CMakeLists.txt [new file with mode: 0644]
resources/google/calendar/CMakeLists.txt [new file with mode: 0644]
resources/google/calendar/Messages.sh [new file with mode: 0644]
resources/google/calendar/calendarresource.cpp [new file with mode: 0644]
resources/google/calendar/calendarresource.h [new file with mode: 0644]
resources/google/calendar/defaultreminderattribute.cpp [new file with mode: 0644]
resources/google/calendar/defaultreminderattribute.h [new file with mode: 0644]
resources/google/calendar/googlecalendarresource.desktop [new file with mode: 0644]
resources/google/calendar/settings.cpp [new file with mode: 0644]
resources/google/calendar/settings.h [new file with mode: 0644]
resources/google/calendar/settingsbase.kcfg [new file with mode: 0644]
resources/google/calendar/settingsbase.kcfgc [new file with mode: 0644]
resources/google/calendar/settingsdialog.cpp [new file with mode: 0644]
resources/google/calendar/settingsdialog.h [new file with mode: 0644]
resources/google/common/googleaccountmanager.cpp [new file with mode: 0644]
resources/google/common/googleaccountmanager.h [new file with mode: 0644]
resources/google/common/googleresource.cpp [new file with mode: 0644]
resources/google/common/googleresource.h [new file with mode: 0644]
resources/google/common/googlesettings.cpp [new file with mode: 0644]
resources/google/common/googlesettings.h [new file with mode: 0644]
resources/google/common/googlesettingsdialog.cpp [new file with mode: 0644]
resources/google/common/googlesettingsdialog.h [new file with mode: 0644]
resources/google/contacts/CMakeLists.txt [new file with mode: 0644]
resources/google/contacts/Messages.sh [new file with mode: 0644]
resources/google/contacts/contactsresource.cpp [new file with mode: 0644]
resources/google/contacts/contactsresource.h [new file with mode: 0644]
resources/google/contacts/googlecontactsresource.desktop [new file with mode: 0644]
resources/google/contacts/settings.cpp [new file with mode: 0644]
resources/google/contacts/settings.h [new file with mode: 0644]
resources/google/contacts/settingsbase.kcfg [new file with mode: 0644]
resources/google/contacts/settingsbase.kcfgc [new file with mode: 0644]
resources/google/contacts/settingsdialog.cpp [new file with mode: 0644]
resources/google/contacts/settingsdialog.h [new file with mode: 0644]
resources/ical/CMakeLists.txt [new file with mode: 0644]
resources/ical/Messages.sh [new file with mode: 0644]
resources/ical/autotests/CMakeLists.txt [new file with mode: 0644]
resources/ical/autotests/event.ical [new file with mode: 0644]
resources/ical/autotests/ical-empty.xml [new file with mode: 0644]
resources/ical/autotests/ical-step1.xml [new file with mode: 0644]
resources/ical/autotests/icaltest.es [new file with mode: 0644]
resources/ical/autotests/task.ical [new file with mode: 0644]
resources/ical/icalresource.cpp [new file with mode: 0644]
resources/ical/icalresource.desktop [new file with mode: 0644]
resources/ical/icalresource.kcfg [new file with mode: 0644]
resources/ical/notes/CMakeLists.txt [new file with mode: 0644]
resources/ical/notes/notesresource.cpp [new file with mode: 0644]
resources/ical/notes/notesresource.desktop [new file with mode: 0644]
resources/ical/notes/notesresource.h [new file with mode: 0644]
resources/ical/notes/notesresource.kcfg [new file with mode: 0644]
resources/ical/notes/settings.kcfgc [new file with mode: 0644]
resources/ical/settings.kcfgc [new file with mode: 0644]
resources/ical/shared/icalresource.cpp [new file with mode: 0644]
resources/ical/shared/icalresource.h [new file with mode: 0644]
resources/ical/shared/icalresourcebase.cpp [new file with mode: 0644]
resources/ical/shared/icalresourcebase.h [new file with mode: 0644]
resources/ical/wizard/CMakeLists.txt [new file with mode: 0644]
resources/ical/wizard/Messages.sh [new file with mode: 0644]
resources/ical/wizard/icalwizard.desktop [new file with mode: 0644]
resources/ical/wizard/icalwizard.es.cmake [new file with mode: 0644]
resources/ical/wizard/icalwizard.ui [new file with mode: 0644]
resources/icaldir/CMakeLists.txt [new file with mode: 0644]
resources/icaldir/Messages.sh [new file with mode: 0644]
resources/icaldir/dirsettingsdialog.cpp [new file with mode: 0644]
resources/icaldir/dirsettingsdialog.h [new file with mode: 0644]
resources/icaldir/icaldirresource.cpp [new file with mode: 0644]
resources/icaldir/icaldirresource.desktop [new file with mode: 0644]
resources/icaldir/icaldirresource.h [new file with mode: 0644]
resources/icaldir/icaldirresource.kcfg [new file with mode: 0644]
resources/icaldir/settings.kcfgc [new file with mode: 0644]
resources/icaldir/settingsdialog.ui [new file with mode: 0644]
resources/imap/CMakeLists.txt [new file with mode: 0644]
resources/imap/Messages.sh [new file with mode: 0755]
resources/imap/addcollectiontask.cpp [new file with mode: 0644]
resources/imap/addcollectiontask.h [new file with mode: 0644]
resources/imap/additemtask.cpp [new file with mode: 0644]
resources/imap/additemtask.h [new file with mode: 0644]
resources/imap/autotests/CMakeLists.txt [new file with mode: 0644]
resources/imap/autotests/dummypasswordrequester.cpp [new file with mode: 0644]
resources/imap/autotests/dummypasswordrequester.h [new file with mode: 0644]
resources/imap/autotests/dummyresourcestate.cpp [new file with mode: 0644]
resources/imap/autotests/dummyresourcestate.h [new file with mode: 0644]
resources/imap/autotests/imaptestbase.cpp [new file with mode: 0644]
resources/imap/autotests/imaptestbase.h [new file with mode: 0644]
resources/imap/autotests/testaddcollectiontask.cpp [new file with mode: 0644]
resources/imap/autotests/testadditemtask.cpp [new file with mode: 0644]
resources/imap/autotests/testchangecollectiontask.cpp [new file with mode: 0644]
resources/imap/autotests/testchangeitemtask.cpp [new file with mode: 0644]
resources/imap/autotests/testexpungecollectiontask.cpp [new file with mode: 0644]
resources/imap/autotests/testmovecollectiontask.cpp [new file with mode: 0644]
resources/imap/autotests/testmoveitemstask.cpp [new file with mode: 0644]
resources/imap/autotests/testremovecollectionrecursivetask.cpp [new file with mode: 0644]
resources/imap/autotests/testresourcetask.cpp [new file with mode: 0644]
resources/imap/autotests/testretrievecollectionmetadatatask.cpp [new file with mode: 0644]
resources/imap/autotests/testretrievecollectionstask.cpp [new file with mode: 0644]
resources/imap/autotests/testretrieveitemstask.cpp [new file with mode: 0644]
resources/imap/autotests/testretrieveitemtask.cpp [new file with mode: 0644]
resources/imap/autotests/testsessionpool.cpp [new file with mode: 0644]
resources/imap/batchfetcher.cpp [new file with mode: 0644]
resources/imap/batchfetcher.h [new file with mode: 0644]
resources/imap/changecollectiontask.cpp [new file with mode: 0644]
resources/imap/changecollectiontask.h [new file with mode: 0644]
resources/imap/changeitemsflagstask.cpp [new file with mode: 0644]
resources/imap/changeitemsflagstask.h [new file with mode: 0644]
resources/imap/changeitemtask.cpp [new file with mode: 0644]
resources/imap/changeitemtask.h [new file with mode: 0644]
resources/imap/collectionmetadatahelper.cpp [new file with mode: 0644]
resources/imap/collectionmetadatahelper.h [new file with mode: 0644]
resources/imap/expungecollectiontask.cpp [new file with mode: 0644]
resources/imap/expungecollectiontask.h [new file with mode: 0644]
resources/imap/highestmodseqattribute.cpp [new file with mode: 0644]
resources/imap/highestmodseqattribute.h [new file with mode: 0644]
resources/imap/imapaccount.cpp [new file with mode: 0644]
resources/imap/imapaccount.h [new file with mode: 0644]
resources/imap/imapflags.cpp [new file with mode: 0644]
resources/imap/imapflags.h [new file with mode: 0644]
resources/imap/imapidlemanager.cpp [new file with mode: 0644]
resources/imap/imapidlemanager.h [new file with mode: 0644]
resources/imap/imapresource.cpp [new file with mode: 0644]
resources/imap/imapresource.desktop [new file with mode: 0644]
resources/imap/imapresource.h [new file with mode: 0644]
resources/imap/imapresource.kcfg [new file with mode: 0644]
resources/imap/imapresourcebase.cpp [new file with mode: 0644]
resources/imap/imapresourcebase.h [new file with mode: 0644]
resources/imap/main.cpp [new file with mode: 0644]
resources/imap/messagehelper.cpp [new file with mode: 0644]
resources/imap/messagehelper.h [new file with mode: 0644]
resources/imap/movecollectiontask.cpp [new file with mode: 0644]
resources/imap/movecollectiontask.h [new file with mode: 0644]
resources/imap/moveitemstask.cpp [new file with mode: 0644]
resources/imap/moveitemstask.h [new file with mode: 0644]
resources/imap/noinferiorsattribute.cpp [new file with mode: 0644]
resources/imap/noinferiorsattribute.h [new file with mode: 0644]
resources/imap/noselectattribute.cpp [new file with mode: 0644]
resources/imap/noselectattribute.h [new file with mode: 0644]
resources/imap/passwordrequesterinterface.cpp [new file with mode: 0644]
resources/imap/passwordrequesterinterface.h [new file with mode: 0644]
resources/imap/removecollectionrecursivetask.cpp [new file with mode: 0644]
resources/imap/removecollectionrecursivetask.h [new file with mode: 0644]
resources/imap/replacemessagejob.cpp [new file with mode: 0644]
resources/imap/replacemessagejob.h [new file with mode: 0644]
resources/imap/resourcestate.cpp [new file with mode: 0644]
resources/imap/resourcestate.h [new file with mode: 0644]
resources/imap/resourcestateinterface.cpp [new file with mode: 0644]
resources/imap/resourcestateinterface.h [new file with mode: 0644]
resources/imap/resourcetask.cpp [new file with mode: 0644]
resources/imap/resourcetask.h [new file with mode: 0644]
resources/imap/retrievecollectionmetadatatask.cpp [new file with mode: 0644]
resources/imap/retrievecollectionmetadatatask.h [new file with mode: 0644]
resources/imap/retrievecollectionstask.cpp [new file with mode: 0644]
resources/imap/retrievecollectionstask.h [new file with mode: 0644]
resources/imap/retrieveitemstask.cpp [new file with mode: 0644]
resources/imap/retrieveitemstask.h [new file with mode: 0644]
resources/imap/retrieveitemtask.cpp [new file with mode: 0644]
resources/imap/retrieveitemtask.h [new file with mode: 0644]
resources/imap/searchtask.cpp [new file with mode: 0644]
resources/imap/searchtask.h [new file with mode: 0644]
resources/imap/serverinfo.ui [new file with mode: 0644]
resources/imap/serverinfodialog.cpp [new file with mode: 0644]
resources/imap/serverinfodialog.h [new file with mode: 0644]
resources/imap/sessionpool.cpp [new file with mode: 0644]
resources/imap/sessionpool.h [new file with mode: 0644]
resources/imap/sessionuiproxy.h [new file with mode: 0644]
resources/imap/settings.cpp [new file with mode: 0644]
resources/imap/settings.h [new file with mode: 0644]
resources/imap/settingsbase.kcfgc [new file with mode: 0644]
resources/imap/settingspasswordrequester.cpp [new file with mode: 0644]
resources/imap/settingspasswordrequester.h [new file with mode: 0644]
resources/imap/setupserver.cpp [new file with mode: 0644]
resources/imap/setupserver.h [new file with mode: 0644]
resources/imap/setupserverview_desktop.ui [new file with mode: 0644]
resources/imap/subscriptiondialog.cpp [new file with mode: 0644]
resources/imap/subscriptiondialog.h [new file with mode: 0644]
resources/imap/tests/CMakeLists.txt [new file with mode: 0644]
resources/imap/tests/testsubscriptiondialog.cpp [new file with mode: 0644]
resources/imap/tracer.cpp [new file with mode: 0644]
resources/imap/tracer.h [new file with mode: 0644]
resources/imap/uidnextattribute.cpp [new file with mode: 0644]
resources/imap/uidnextattribute.h [new file with mode: 0644]
resources/imap/uidvalidityattribute.cpp [new file with mode: 0644]
resources/imap/uidvalidityattribute.h [new file with mode: 0644]
resources/imap/wizard/CMakeLists.txt [new file with mode: 0644]
resources/imap/wizard/Messages.sh [new file with mode: 0755]
resources/imap/wizard/imapwizard.desktop [new file with mode: 0644]
resources/imap/wizard/imapwizard.es [new file with mode: 0644]
resources/imap/wizard/imapwizard.ui [new file with mode: 0644]
resources/kalarm/CMakeLists.txt [new file with mode: 0644]
resources/kalarm/Messages.sh [new file with mode: 0755]
resources/kalarm/kalarm/CMakeLists.txt [new file with mode: 0644]
resources/kalarm/kalarm/kalarmresource.cpp [new file with mode: 0644]
resources/kalarm/kalarm/kalarmresource.desktop [new file with mode: 0644]
resources/kalarm/kalarm/kalarmresource.h [new file with mode: 0644]
resources/kalarm/kalarm/kalarmresource.kcfg [new file with mode: 0644]
resources/kalarm/kalarm/settings.kcfgc [new file with mode: 0644]
resources/kalarm/kalarmdir/CMakeLists.txt [new file with mode: 0644]
resources/kalarm/kalarmdir/autoqpointer.h [new file with mode: 0644]
resources/kalarm/kalarmdir/kalarmdirresource.cpp [new file with mode: 0644]
resources/kalarm/kalarmdir/kalarmdirresource.desktop [new file with mode: 0644]
resources/kalarm/kalarmdir/kalarmdirresource.h [new file with mode: 0644]
resources/kalarm/kalarmdir/kalarmdirresource.kcfg [new file with mode: 0644]
resources/kalarm/kalarmdir/settings.kcfgc [new file with mode: 0644]
resources/kalarm/kalarmdir/settingsdialog.cpp [new file with mode: 0644]
resources/kalarm/kalarmdir/settingsdialog.h [new file with mode: 0644]
resources/kalarm/kalarmdir/settingsdialog.ui [new file with mode: 0644]
resources/kalarm/shared/alarmtyperadiowidget.cpp [new file with mode: 0644]
resources/kalarm/shared/alarmtyperadiowidget.h [new file with mode: 0644]
resources/kalarm/shared/alarmtyperadiowidget.ui [new file with mode: 0644]
resources/kalarm/shared/alarmtypewidget.cpp [new file with mode: 0644]
resources/kalarm/shared/alarmtypewidget.h [new file with mode: 0644]
resources/kalarm/shared/alarmtypewidget.ui [new file with mode: 0644]
resources/kalarm/shared/kalarmresourcecommon.cpp [new file with mode: 0644]
resources/kalarm/shared/kalarmresourcecommon.h [new file with mode: 0644]
resources/kolab/64-apps-kolab.png [new file with mode: 0644]
resources/kolab/CMakeLists.txt [new file with mode: 0644]
resources/kolab/kolabaddtagtask.cpp [new file with mode: 0644]
resources/kolab/kolabaddtagtask.h [new file with mode: 0644]
resources/kolab/kolabchangeitemsrelationstask.cpp [new file with mode: 0644]
resources/kolab/kolabchangeitemsrelationstask.h [new file with mode: 0644]
resources/kolab/kolabchangeitemstagstask.cpp [new file with mode: 0644]
resources/kolab/kolabchangeitemstagstask.h [new file with mode: 0644]
resources/kolab/kolabchangetagtask.cpp [new file with mode: 0644]
resources/kolab/kolabchangetagtask.h [new file with mode: 0644]
resources/kolab/kolabhelpers.cpp [new file with mode: 0644]
resources/kolab/kolabhelpers.h [new file with mode: 0644]
resources/kolab/kolabmessagehelper.cpp [new file with mode: 0644]
resources/kolab/kolabmessagehelper.h [new file with mode: 0644]
resources/kolab/kolabrelationresourcetask.cpp [new file with mode: 0644]
resources/kolab/kolabrelationresourcetask.h [new file with mode: 0644]
resources/kolab/kolabremovetagtask.cpp [new file with mode: 0644]
resources/kolab/kolabremovetagtask.h [new file with mode: 0644]
resources/kolab/kolabresource.cpp [new file with mode: 0644]
resources/kolab/kolabresource.desktop [new file with mode: 0644]
resources/kolab/kolabresource.h [new file with mode: 0644]
resources/kolab/kolabresourcestate.cpp [new file with mode: 0644]
resources/kolab/kolabresourcestate.h [new file with mode: 0644]
resources/kolab/kolabretrievecollectionstask.cpp [new file with mode: 0644]
resources/kolab/kolabretrievecollectionstask.h [new file with mode: 0644]
resources/kolab/kolabretrievetagstask.cpp [new file with mode: 0644]
resources/kolab/kolabretrievetagstask.h [new file with mode: 0644]
resources/kolab/kolabsettings.cpp [new file with mode: 0644]
resources/kolab/kolabsettings.h [new file with mode: 0644]
resources/kolab/tagchangehelper.cpp [new file with mode: 0644]
resources/kolab/tagchangehelper.h [new file with mode: 0644]
resources/kolab/tests/CMakeLists.txt [new file with mode: 0644]
resources/kolab/tests/imaptestbase.cpp [new file with mode: 0644]
resources/kolab/tests/imaptestbase.h [new file with mode: 0644]
resources/kolab/tests/testchangeitemstagstask.cpp [new file with mode: 0644]
resources/kolab/tests/testretrievetagstask.cpp [new file with mode: 0644]
resources/kolab/tests/unittestenv/config-mysql-db.xml [new file with mode: 0644]
resources/kolab/tests/unittestenv/config-mysql-fs.xml [new file with mode: 0644]
resources/kolab/tests/unittestenv/config-postgresql-db.xml [new file with mode: 0644]
resources/kolab/tests/unittestenv/config-postgresql-fs.xml [new file with mode: 0644]
resources/kolab/tests/unittestenv/config-sqlite-db.xml [new file with mode: 0644]
resources/kolab/tests/unittestenv/kdehome/share/config/akonadi-firstrunrc [new file with mode: 0644]
resources/kolab/tests/unittestenv/kdehome/share/config/akonadi_knut_resource_0rc [new file with mode: 0644]
resources/kolab/tests/unittestenv/kdehome/share/config/kdebugrc [new file with mode: 0755]
resources/kolab/tests/unittestenv/kdehome/share/config/kdedrc [new file with mode: 0644]
resources/kolab/tests/unittestenv/kdehome/testdata-res1.xml [new file with mode: 0644]
resources/kolab/tests/unittestenv/xdgconfig-mysql.db/akonadi/akonadiserverrc [new file with mode: 0644]
resources/kolab/tests/unittestenv/xdgconfig-mysql.fs/akonadi/akonadiserverrc [new file with mode: 0644]
resources/kolab/tests/unittestenv/xdgconfig-postgresql.db/akonadi/akonadiserverrc [new file with mode: 0644]
resources/kolab/tests/unittestenv/xdgconfig-postgresql.fs/akonadi/akonadiserverrc [new file with mode: 0644]
resources/kolab/tests/unittestenv/xdgconfig-sqlite.db/akonadi/akonadiserverrc [new file with mode: 0644]
resources/kolab/updatemessagejob.cpp [new file with mode: 0644]
resources/kolab/updatemessagejob.h [new file with mode: 0644]
resources/kolab/wizard/CMakeLists.txt [new file with mode: 0644]
resources/kolab/wizard/Messages.sh [new file with mode: 0644]
resources/kolab/wizard/kolabwizard.desktop [new file with mode: 0644]
resources/kolab/wizard/kolabwizard.es [new file with mode: 0644]
resources/kolab/wizard/kolabwizard.ui [new file with mode: 0644]
resources/kolab/wizard/kolabwizard2.ui [new file with mode: 0644]
resources/maildir/CMakeLists.txt [new file with mode: 0644]
resources/maildir/Messages.sh [new file with mode: 0644]
resources/maildir/autotests/CMakeLists.txt [new file with mode: 0644]
resources/maildir/autotests/maildir-empty.xml [new file with mode: 0644]
resources/maildir/autotests/maildir-step1.xml [new file with mode: 0644]
resources/maildir/autotests/maildir-step2.xml [new file with mode: 0644]
resources/maildir/autotests/maildir.js [new file with mode: 0644]
resources/maildir/autotests/maildir.xml [new file with mode: 0644]
resources/maildir/autotests/maildir/.root.directory/.child1.directory/grandchild/cur/1237726881.6570.rfoxg!2,S [new file with mode: 0644]
resources/maildir/autotests/maildir/.root.directory/.child1.directory/grandchild/new/.keep [new file with mode: 0644]
resources/maildir/autotests/maildir/.root.directory/.child1.directory/grandchild/tmp/.keep [new file with mode: 0644]
resources/maildir/autotests/maildir/.root.directory/child1/cur/1237726858.6570.dtdn4!2,S [new file with mode: 0644]
resources/maildir/autotests/maildir/.root.directory/child1/cur/1237726875.6570.R4KOW!2,S [new file with mode: 0644]
resources/maildir/autotests/maildir/.root.directory/child1/new/.keep [new file with mode: 0644]
resources/maildir/autotests/maildir/.root.directory/child1/tmp/.keep [new file with mode: 0644]
resources/maildir/autotests/maildir/.root.directory/child2/.keep [new file with mode: 0644]
resources/maildir/autotests/maildir/.root.directory/child2/cur/.keep [new file with mode: 0644]
resources/maildir/autotests/maildir/.root.directory/child2/new/.keep [new file with mode: 0644]
resources/maildir/autotests/maildir/.root.directory/child2/tmp/.keep [new file with mode: 0644]
resources/maildir/autotests/maildir/root/cur/1237726845.6570.BejQg!2,S [new file with mode: 0644]
resources/maildir/autotests/maildir/root/new/.keep [new file with mode: 0644]
resources/maildir/autotests/maildir/root/tmp/.keep [new file with mode: 0644]
resources/maildir/autotests/synctest.cpp [new file with mode: 0644]
resources/maildir/autotests/synctest.h [new file with mode: 0644]
resources/maildir/autotests/testmail.mbox [new file with mode: 0644]
resources/maildir/autotests/unittestenv/config.xml [new file with mode: 0644]
resources/maildir/autotests/unittestenv/kdehome/share/config/akonadi-firstrunrc [new file with mode: 0644]
resources/maildir/autotests/unittestenv/kdehome/share/config/akonadi_maildir_resource_0rc [new file with mode: 0644]
resources/maildir/autotests/unittestenv/kdehome/share/config/kdebugrc [new file with mode: 0644]
resources/maildir/autotests/unittestenv/kdehome/share/config/kdedrc [new file with mode: 0644]
resources/maildir/autotests/unittestenv/kdehome/share/config/kwalletrc [new file with mode: 0644]
resources/maildir/autotests/unittestenv/kdehome/share/config/qttestrc [new file with mode: 0644]
resources/maildir/autotests/unittestenv/kdehome/testdata.xml [new file with mode: 0644]
resources/maildir/autotests/unittestenv/xdgconfig/akonadi/akonadiserverrc [new file with mode: 0644]
resources/maildir/autotests/unittestenv/xdglocal/.keep [new file with mode: 0644]
resources/maildir/configdialog.cpp [new file with mode: 0644]
resources/maildir/configdialog.h [new file with mode: 0644]
resources/maildir/libmaildir/CMakeLists.txt [new file with mode: 0644]
resources/maildir/libmaildir/autotests/CMakeLists.txt [new file with mode: 0644]
resources/maildir/libmaildir/autotests/testmaildir.cpp [new file with mode: 0644]
resources/maildir/libmaildir/autotests/testmaildir.h [new file with mode: 0644]
resources/maildir/libmaildir/keycache.cpp [new file with mode: 0644]
resources/maildir/libmaildir/keycache.h [new file with mode: 0644]
resources/maildir/libmaildir/maildir.cpp [new file with mode: 0644]
resources/maildir/libmaildir/maildir.h [new file with mode: 0644]
resources/maildir/maildirresource.cpp [new file with mode: 0644]
resources/maildir/maildirresource.desktop [new file with mode: 0644]
resources/maildir/maildirresource.h [new file with mode: 0644]
resources/maildir/maildirresource.kcfg [new file with mode: 0644]
resources/maildir/main.cpp [new file with mode: 0644]
resources/maildir/retrieveitemsjob.cpp [new file with mode: 0644]
resources/maildir/retrieveitemsjob.h [new file with mode: 0644]
resources/maildir/settings.kcfgc [new file with mode: 0644]
resources/maildir/settings.ui [new file with mode: 0644]
resources/maildir/wizard/CMakeLists.txt [new file with mode: 0644]
resources/maildir/wizard/Messages.sh [new file with mode: 0644]
resources/maildir/wizard/maildirwizard.desktop [new file with mode: 0644]
resources/maildir/wizard/maildirwizard.es [new file with mode: 0644]
resources/maildir/wizard/maildirwizard.ui [new file with mode: 0644]
resources/mbox/CMakeLists.txt [new file with mode: 0644]
resources/mbox/Messages.sh [new file with mode: 0644]
resources/mbox/autotests/CMakeLists.txt [new file with mode: 0644]
resources/mbox/autotests/deleteitemsattributetest.cpp [new file with mode: 0644]
resources/mbox/autotests/deleteitemsattributetest.h [new file with mode: 0644]
resources/mbox/compactpage.cpp [new file with mode: 0644]
resources/mbox/compactpage.h [new file with mode: 0644]
resources/mbox/compactpage.ui [new file with mode: 0644]
resources/mbox/deleteditemsattribute.cpp [new file with mode: 0644]
resources/mbox/deleteditemsattribute.h [new file with mode: 0644]
resources/mbox/lockfilepage.ui [new file with mode: 0644]
resources/mbox/lockmethodpage.cpp [new file with mode: 0644]
resources/mbox/lockmethodpage.h [new file with mode: 0644]
resources/mbox/mboxresource.cpp [new file with mode: 0644]
resources/mbox/mboxresource.desktop [new file with mode: 0644]
resources/mbox/mboxresource.h [new file with mode: 0644]
resources/mbox/mboxresource.kcfg [new file with mode: 0644]
resources/mbox/settings.kcfgc [new file with mode: 0644]
resources/mbox/wizard/CMakeLists.txt [new file with mode: 0644]
resources/mbox/wizard/Messages.sh [new file with mode: 0644]
resources/mbox/wizard/mailboxwizard.desktop [new file with mode: 0644]
resources/mbox/wizard/mailboxwizard.es [new file with mode: 0644]
resources/mbox/wizard/mailboxwizard.ui [new file with mode: 0644]
resources/mixedmaildir/CMakeLists.txt [new file with mode: 0644]
resources/mixedmaildir/Messages.sh [new file with mode: 0644]
resources/mixedmaildir/autotests/CMakeLists.txt [new file with mode: 0644]
resources/mixedmaildir/autotests/collectioncreatetest.cpp [new file with mode: 0644]
resources/mixedmaildir/autotests/collectiondeletetest.cpp [new file with mode: 0644]
resources/mixedmaildir/autotests/collectionfetchtest.cpp [new file with mode: 0644]
resources/mixedmaildir/autotests/collectionmodifytest.cpp [new file with mode: 0644]
resources/mixedmaildir/autotests/collectionmovetest.cpp [new file with mode: 0644]
resources/mixedmaildir/autotests/data/.dimap.index [new file with mode: 0644]
resources/mixedmaildir/autotests/data/.maildir-tagged.index [new file with mode: 0644]
resources/mixedmaildir/autotests/data/.maildir.index [new file with mode: 0644]
resources/mixedmaildir/autotests/data/.mbox-tagged.index [new file with mode: 0644]
resources/mixedmaildir/autotests/data/.mbox-unpurged.index [new file with mode: 0644]
resources/mixedmaildir/autotests/data/.mbox.index [new file with mode: 0644]
resources/mixedmaildir/autotests/data/README [new file with mode: 0644]
resources/mixedmaildir/autotests/data/dimap/cur/1279980064.4595.LUBVK [new file with mode: 0644]
resources/mixedmaildir/autotests/data/dimap/cur/1279980064.4595.RTmAd_2,S [new file with mode: 0644]
resources/mixedmaildir/autotests/data/dimap/cur/1279980064.4595.g8PCJ [new file with mode: 0644]
resources/mixedmaildir/autotests/data/dimap/cur/1279980064.4595.qs6V9_2,S [new file with mode: 0644]
resources/mixedmaildir/autotests/data/dimap/new/.keep [new file with mode: 0644]
resources/mixedmaildir/autotests/data/dimap/tmp/.keep [new file with mode: 0644]
resources/mixedmaildir/autotests/data/maildir-tagged/cur/1279982188.18722.6qZsA_2,S [new file with mode: 0644]
resources/mixedmaildir/autotests/data/maildir-tagged/cur/1279982188.18722.Xdz3R_2,S [new file with mode: 0644]
resources/mixedmaildir/autotests/data/maildir-tagged/cur/1279982188.18722.f0l49_2,S [new file with mode: 0644]
resources/mixedmaildir/autotests/data/maildir-tagged/cur/1279982188.18722.kwx1b_2,S [new file with mode: 0644]
resources/mixedmaildir/autotests/data/maildir-tagged/new/.keep [new file with mode: 0644]
resources/mixedmaildir/autotests/data/maildir-tagged/tmp/.keep [new file with mode: 0644]
resources/mixedmaildir/autotests/data/maildir/cur/1279979617.4595.bwXSm [new file with mode: 0644]
resources/mixedmaildir/autotests/data/maildir/cur/1279979618.4595.CStza_2,S [new file with mode: 0644]
resources/mixedmaildir/autotests/data/maildir/cur/1279979618.4595.DUl0I_2,S [new file with mode: 0644]
resources/mixedmaildir/autotests/data/maildir/cur/1279979618.4595.pY5ny [new file with mode: 0644]
resources/mixedmaildir/autotests/data/maildir/new/.keep [new file with mode: 0644]
resources/mixedmaildir/autotests/data/maildir/tmp/.keep [new file with mode: 0644]
resources/mixedmaildir/autotests/data/mbox [new file with mode: 0644]
resources/mixedmaildir/autotests/data/mbox-tagged [new file with mode: 0644]
resources/mixedmaildir/autotests/data/mbox-unpurged [new file with mode: 0644]
resources/mixedmaildir/autotests/itemcreatetest.cpp [new file with mode: 0644]
resources/mixedmaildir/autotests/itemdeletetest.cpp [new file with mode: 0644]
resources/mixedmaildir/autotests/itemfetchtest.cpp [new file with mode: 0644]
resources/mixedmaildir/autotests/itemmodifytest.cpp [new file with mode: 0644]
resources/mixedmaildir/autotests/itemmovetest.cpp [new file with mode: 0644]
resources/mixedmaildir/autotests/storecompacttest.cpp [new file with mode: 0644]
resources/mixedmaildir/autotests/templatemethodstest.cpp [new file with mode: 0644]
resources/mixedmaildir/autotests/testdata.qrc [new file with mode: 0644]
resources/mixedmaildir/autotests/testdatatest.cpp [new file with mode: 0644]
resources/mixedmaildir/autotests/testdatautil.cpp [new file with mode: 0644]
resources/mixedmaildir/autotests/testdatautil.h [new file with mode: 0644]
resources/mixedmaildir/compactchangehelper.cpp [new file with mode: 0644]
resources/mixedmaildir/compactchangehelper.h [new file with mode: 0644]
resources/mixedmaildir/configdialog.cpp [new file with mode: 0644]
resources/mixedmaildir/configdialog.h [new file with mode: 0644]
resources/mixedmaildir/kmindexreader/CMakeLists.txt [new file with mode: 0644]
resources/mixedmaildir/kmindexreader/autotests/CMakeLists.txt [new file with mode: 0644]
resources/mixedmaildir/kmindexreader/autotests/TestIdxReader_data.h [new file with mode: 0644]
resources/mixedmaildir/kmindexreader/autotests/data/.keep [new file with mode: 0644]
resources/mixedmaildir/kmindexreader/autotests/testidxreader.cpp [new file with mode: 0644]
resources/mixedmaildir/kmindexreader/autotests/testidxreader.h [new file with mode: 0644]
resources/mixedmaildir/kmindexreader/kmindexreader.cpp [new file with mode: 0644]
resources/mixedmaildir/kmindexreader/kmindexreader.h [new file with mode: 0644]
resources/mixedmaildir/mixedmaildir_debug.cpp [new file with mode: 0644]
resources/mixedmaildir/mixedmaildir_debug.h [new file with mode: 0644]
resources/mixedmaildir/mixedmaildirresource.cpp [new file with mode: 0644]
resources/mixedmaildir/mixedmaildirresource.desktop [new file with mode: 0644]
resources/mixedmaildir/mixedmaildirresource.h [new file with mode: 0644]
resources/mixedmaildir/mixedmaildirresource.kcfg [new file with mode: 0644]
resources/mixedmaildir/mixedmaildirresource_debug.cpp [new file with mode: 0644]
resources/mixedmaildir/mixedmaildirresource_debug.h [new file with mode: 0644]
resources/mixedmaildir/mixedmaildirstore.cpp [new file with mode: 0644]
resources/mixedmaildir/mixedmaildirstore.h [new file with mode: 0644]
resources/mixedmaildir/retrieveitemsjob.cpp [new file with mode: 0644]
resources/mixedmaildir/retrieveitemsjob.h [new file with mode: 0644]
resources/mixedmaildir/settings.kcfgc [new file with mode: 0644]
resources/mixedmaildir/settings.ui [new file with mode: 0644]
resources/openxchange/CMakeLists.txt [new file with mode: 0644]
resources/openxchange/Messages.sh [new file with mode: 0644]
resources/openxchange/configdialog.cpp [new file with mode: 0644]
resources/openxchange/configdialog.h [new file with mode: 0644]
resources/openxchange/configdialog.ui [new file with mode: 0644]
resources/openxchange/icons/128-apps-ox.png [new file with mode: 0644]
resources/openxchange/icons/16-apps-ox.png [new file with mode: 0644]
resources/openxchange/icons/32-apps-ox.png [new file with mode: 0644]
resources/openxchange/icons/48-apps-ox.png [new file with mode: 0644]
resources/openxchange/icons/64-apps-ox.png [new file with mode: 0644]
resources/openxchange/icons/CMakeLists.txt [new file with mode: 0644]
resources/openxchange/openxchangeresource.cpp [new file with mode: 0644]
resources/openxchange/openxchangeresource.desktop [new file with mode: 0644]
resources/openxchange/openxchangeresource.h [new file with mode: 0644]
resources/openxchange/openxchangeresource.kcfg [new file with mode: 0644]
resources/openxchange/oxa/connectiontestjob.cpp [new file with mode: 0644]
resources/openxchange/oxa/connectiontestjob.h [new file with mode: 0644]
resources/openxchange/oxa/contactutils.cpp [new file with mode: 0644]
resources/openxchange/oxa/contactutils.h [new file with mode: 0644]
resources/openxchange/oxa/davmanager.cpp [new file with mode: 0644]
resources/openxchange/oxa/davmanager.h [new file with mode: 0644]
resources/openxchange/oxa/davutils.cpp [new file with mode: 0644]
resources/openxchange/oxa/davutils.h [new file with mode: 0644]
resources/openxchange/oxa/folder.cpp [new file with mode: 0644]
resources/openxchange/oxa/folder.h [new file with mode: 0644]
resources/openxchange/oxa/foldercreatejob.cpp [new file with mode: 0644]
resources/openxchange/oxa/foldercreatejob.h [new file with mode: 0644]
resources/openxchange/oxa/folderdeletejob.cpp [new file with mode: 0644]
resources/openxchange/oxa/folderdeletejob.h [new file with mode: 0644]
resources/openxchange/oxa/foldermodifyjob.cpp [new file with mode: 0644]
resources/openxchange/oxa/foldermodifyjob.h [new file with mode: 0644]
resources/openxchange/oxa/foldermovejob.cpp [new file with mode: 0644]
resources/openxchange/oxa/foldermovejob.h [new file with mode: 0644]
resources/openxchange/oxa/folderrequestjob.cpp [new file with mode: 0644]
resources/openxchange/oxa/folderrequestjob.h [new file with mode: 0644]
resources/openxchange/oxa/foldersrequestdeltajob.cpp [new file with mode: 0644]
resources/openxchange/oxa/foldersrequestdeltajob.h [new file with mode: 0644]
resources/openxchange/oxa/foldersrequestjob.cpp [new file with mode: 0644]
resources/openxchange/oxa/foldersrequestjob.h [new file with mode: 0644]
resources/openxchange/oxa/folderutils.cpp [new file with mode: 0644]
resources/openxchange/oxa/folderutils.h [new file with mode: 0644]
resources/openxchange/oxa/incidenceutils.cpp [new file with mode: 0644]
resources/openxchange/oxa/incidenceutils.h [new file with mode: 0644]
resources/openxchange/oxa/object.cpp [new file with mode: 0644]
resources/openxchange/oxa/object.h [new file with mode: 0644]
resources/openxchange/oxa/objectcreatejob.cpp [new file with mode: 0644]
resources/openxchange/oxa/objectcreatejob.h [new file with mode: 0644]
resources/openxchange/oxa/objectdeletejob.cpp [new file with mode: 0644]
resources/openxchange/oxa/objectdeletejob.h [new file with mode: 0644]
resources/openxchange/oxa/objectmodifyjob.cpp [new file with mode: 0644]
resources/openxchange/oxa/objectmodifyjob.h [new file with mode: 0644]
resources/openxchange/oxa/objectmovejob.cpp [new file with mode: 0644]
resources/openxchange/oxa/objectmovejob.h [new file with mode: 0644]
resources/openxchange/oxa/objectrequestjob.cpp [new file with mode: 0644]
resources/openxchange/oxa/objectrequestjob.h [new file with mode: 0644]
resources/openxchange/oxa/objectsrequestdeltajob.cpp [new file with mode: 0644]
resources/openxchange/oxa/objectsrequestdeltajob.h [new file with mode: 0644]
resources/openxchange/oxa/objectsrequestjob.cpp [new file with mode: 0644]
resources/openxchange/oxa/objectsrequestjob.h [new file with mode: 0644]
resources/openxchange/oxa/objectutils.cpp [new file with mode: 0644]
resources/openxchange/oxa/objectutils.h [new file with mode: 0644]
resources/openxchange/oxa/oxerrors.cpp [new file with mode: 0644]
resources/openxchange/oxa/oxerrors.h [new file with mode: 0644]
resources/openxchange/oxa/oxutils.cpp [new file with mode: 0644]
resources/openxchange/oxa/oxutils.h [new file with mode: 0644]
resources/openxchange/oxa/updateusersjob.cpp [new file with mode: 0644]
resources/openxchange/oxa/updateusersjob.h [new file with mode: 0644]
resources/openxchange/oxa/user.cpp [new file with mode: 0644]
resources/openxchange/oxa/user.h [new file with mode: 0644]
resources/openxchange/oxa/useridrequestjob.cpp [new file with mode: 0644]
resources/openxchange/oxa/useridrequestjob.h [new file with mode: 0644]
resources/openxchange/oxa/users.cpp [new file with mode: 0644]
resources/openxchange/oxa/users.h [new file with mode: 0644]
resources/openxchange/oxa/usersrequestjob.cpp [new file with mode: 0644]
resources/openxchange/oxa/usersrequestjob.h [new file with mode: 0644]
resources/openxchange/settings.kcfgc [new file with mode: 0644]
resources/pop3/CMakeLists.txt [new file with mode: 0644]
resources/pop3/Messages.sh [new file with mode: 0644]
resources/pop3/TODO [new file with mode: 0644]
resources/pop3/accountdialog.cpp [new file with mode: 0644]
resources/pop3/accountdialog.h [new file with mode: 0644]
resources/pop3/autotests/CMakeLists.txt [new file with mode: 0644]
resources/pop3/autotests/fakeserver/fakeserver.cpp [new file with mode: 0644]
resources/pop3/autotests/fakeserver/fakeserver.h [new file with mode: 0644]
resources/pop3/autotests/pop3test.cpp [new file with mode: 0644]
resources/pop3/autotests/pop3test.h [new file with mode: 0644]
resources/pop3/autotests/unittestenv/config.xml [new file with mode: 0644]
resources/pop3/autotests/unittestenv/kdehome/share/apps/.keep [new file with mode: 0644]
resources/pop3/autotests/unittestenv/kdehome/share/config/akonadi-firstrunrc [new file with mode: 0644]
resources/pop3/autotests/unittestenv/kdehome/share/config/kdebugrc [new file with mode: 0755]
resources/pop3/autotests/unittestenv/xdgconfig/akonadi/akonadiserverrc [new file with mode: 0644]
resources/pop3/autotests/unittestenv/xdglocal/.keep [new file with mode: 0644]
resources/pop3/jobs.cpp [new file with mode: 0644]
resources/pop3/jobs.h [new file with mode: 0644]
resources/pop3/metatype.h [new file with mode: 0644]
resources/pop3/pop3resource.cpp [new file with mode: 0644]
resources/pop3/pop3resource.desktop [new file with mode: 0644]
resources/pop3/pop3resource.h [new file with mode: 0644]
resources/pop3/popsettings.ui [new file with mode: 0644]
resources/pop3/settings.cpp [new file with mode: 0644]
resources/pop3/settings.h [new file with mode: 0644]
resources/pop3/settings.kcfg [new file with mode: 0644]
resources/pop3/settingsbase.kcfgc [new file with mode: 0644]
resources/pop3/wizard/CMakeLists.txt [new file with mode: 0644]
resources/pop3/wizard/Messages.sh [new file with mode: 0644]
resources/pop3/wizard/pop3wizard.desktop [new file with mode: 0644]
resources/pop3/wizard/pop3wizard.es [new file with mode: 0644]
resources/pop3/wizard/pop3wizard.ui [new file with mode: 0644]
resources/shared/CMakeLists.txt [new file with mode: 0644]
resources/shared/filestore/CMakeLists.txt [new file with mode: 0644]
resources/shared/filestore/Messages.sh [new file with mode: 0644]
resources/shared/filestore/abstractlocalstore.cpp [new file with mode: 0644]
resources/shared/filestore/abstractlocalstore.h [new file with mode: 0644]
resources/shared/filestore/autotests/CMakeLists.txt [new file with mode: 0644]
resources/shared/filestore/autotests/abstractlocalstoretest.cpp [new file with mode: 0644]
resources/shared/filestore/collectioncreatejob.cpp [new file with mode: 0644]
resources/shared/filestore/collectioncreatejob.h [new file with mode: 0644]
resources/shared/filestore/collectiondeletejob.cpp [new file with mode: 0644]
resources/shared/filestore/collectiondeletejob.h [new file with mode: 0644]
resources/shared/filestore/collectionfetchjob.cpp [new file with mode: 0644]
resources/shared/filestore/collectionfetchjob.h [new file with mode: 0644]
resources/shared/filestore/collectionmodifyjob.cpp [new file with mode: 0644]
resources/shared/filestore/collectionmodifyjob.h [new file with mode: 0644]
resources/shared/filestore/collectionmovejob.cpp [new file with mode: 0644]
resources/shared/filestore/collectionmovejob.h [new file with mode: 0644]
resources/shared/filestore/entitycompactchangeattribute.cpp [new file with mode: 0644]
resources/shared/filestore/entitycompactchangeattribute.h [new file with mode: 0644]
resources/shared/filestore/itemcreatejob.cpp [new file with mode: 0644]
resources/shared/filestore/itemcreatejob.h [new file with mode: 0644]
resources/shared/filestore/itemdeletejob.cpp [new file with mode: 0644]
resources/shared/filestore/itemdeletejob.h [new file with mode: 0644]
resources/shared/filestore/itemfetchjob.cpp [new file with mode: 0644]
resources/shared/filestore/itemfetchjob.h [new file with mode: 0644]
resources/shared/filestore/itemmodifyjob.cpp [new file with mode: 0644]
resources/shared/filestore/itemmodifyjob.h [new file with mode: 0644]
resources/shared/filestore/itemmovejob.cpp [new file with mode: 0644]
resources/shared/filestore/itemmovejob.h [new file with mode: 0644]
resources/shared/filestore/job.cpp [new file with mode: 0644]
resources/shared/filestore/job.h [new file with mode: 0644]
resources/shared/filestore/session.cpp [new file with mode: 0644]
resources/shared/filestore/session_p.h [new file with mode: 0644]
resources/shared/filestore/sessionimpls.cpp [new file with mode: 0644]
resources/shared/filestore/sessionimpls_p.h [new file with mode: 0644]
resources/shared/filestore/storecompactjob.cpp [new file with mode: 0644]
resources/shared/filestore/storecompactjob.h [new file with mode: 0644]
resources/shared/filestore/storeinterface.h [new file with mode: 0644]
resources/shared/singlefileresource/CMakeLists.txt [new file with mode: 0644]
resources/shared/singlefileresource/Messages.sh [new file with mode: 0644]
resources/shared/singlefileresource/autotests/CMakeLists.txt [new file with mode: 0644]
resources/shared/singlefileresource/autotests/collectionannotationattributetest.cpp [new file with mode: 0644]
resources/shared/singlefileresource/autotests/imapaclattributetest.cpp [new file with mode: 0644]
resources/shared/singlefileresource/collectionannotationsattribute.cpp [new file with mode: 0644]
resources/shared/singlefileresource/collectionannotationsattribute.h [new file with mode: 0644]
resources/shared/singlefileresource/collectionflagsattribute.cpp [new file with mode: 0644]
resources/shared/singlefileresource/collectionflagsattribute.h [new file with mode: 0644]
resources/shared/singlefileresource/createandsettagsjob.cpp [new file with mode: 0644]
resources/shared/singlefileresource/createandsettagsjob.h [new file with mode: 0644]
resources/shared/singlefileresource/getcredentialsjob.cpp [new file with mode: 0644]
resources/shared/singlefileresource/getcredentialsjob.h [new file with mode: 0644]
resources/shared/singlefileresource/imapaclattribute.cpp [new file with mode: 0644]
resources/shared/singlefileresource/imapaclattribute.h [new file with mode: 0644]
resources/shared/singlefileresource/imapquotaattribute.cpp [new file with mode: 0644]
resources/shared/singlefileresource/imapquotaattribute.h [new file with mode: 0644]
resources/shared/singlefileresource/settingsdialog.ui [new file with mode: 0644]
resources/shared/singlefileresource/singlefileresource.h [new file with mode: 0644]
resources/shared/singlefileresource/singlefileresourcebase.cpp [new file with mode: 0644]
resources/shared/singlefileresource/singlefileresourcebase.h [new file with mode: 0644]
resources/shared/singlefileresource/singlefileresourceconfigdialog.h [new file with mode: 0644]
resources/shared/singlefileresource/singlefileresourceconfigdialog.ui [new file with mode: 0644]
resources/shared/singlefileresource/singlefileresourceconfigdialog_desktop.ui [new file with mode: 0644]
resources/shared/singlefileresource/singlefileresourceconfigdialogbase.cpp [new file with mode: 0644]
resources/shared/singlefileresource/singlefileresourceconfigdialogbase.h [new file with mode: 0644]
resources/vcard/CMakeLists.txt [new file with mode: 0644]
resources/vcard/Messages.sh [new file with mode: 0644]
resources/vcard/autotests/CMakeLists.txt [new file with mode: 0644]
resources/vcard/autotests/vcardtest-readonly.js [new file with mode: 0644]
resources/vcard/autotests/vcardtest-readonly.xml [new file with mode: 0644]
resources/vcard/autotests/vcardtest.js [new file with mode: 0644]
resources/vcard/autotests/vcardtest.vcf [new file with mode: 0644]
resources/vcard/autotests/vcardtest.xml [new file with mode: 0644]
resources/vcard/settings.kcfgc [new file with mode: 0644]
resources/vcard/vcardresource.cpp [new file with mode: 0644]
resources/vcard/vcardresource.desktop [new file with mode: 0644]
resources/vcard/vcardresource.h [new file with mode: 0644]
resources/vcard/vcardresource.kcfg [new file with mode: 0644]
resources/vcard/wizard/CMakeLists.txt [new file with mode: 0644]
resources/vcard/wizard/Messages.sh [new file with mode: 0644]
resources/vcard/wizard/vcardwizard.desktop [new file with mode: 0644]
resources/vcard/wizard/vcardwizard.es.cmake [new file with mode: 0644]
resources/vcard/wizard/vcardwizard.ui [new file with mode: 0644]
resources/vcarddir/CMakeLists.txt [new file with mode: 0644]
resources/vcarddir/Messages.sh [new file with mode: 0644]
resources/vcarddir/dirsettingsdialog.cpp [new file with mode: 0644]
resources/vcarddir/dirsettingsdialog.h [new file with mode: 0644]
resources/vcarddir/settings.kcfgc [new file with mode: 0644]
resources/vcarddir/vcarddirresource.cpp [new file with mode: 0644]
resources/vcarddir/vcarddirresource.desktop [new file with mode: 0644]
resources/vcarddir/vcarddirresource.h [new file with mode: 0644]
resources/vcarddir/vcarddirresource.kcfg [new file with mode: 0644]
resources/vcarddir/wizard/CMakeLists.txt [new file with mode: 0644]
resources/vcarddir/wizard/Messages.sh [new file with mode: 0644]
resources/vcarddir/wizard/vcarddirwizard.desktop [new file with mode: 0644]
resources/vcarddir/wizard/vcarddirwizard.es.cmake [new file with mode: 0644]
resources/vcarddir/wizard/vcarddirwizard.ui [new file with mode: 0644]

diff --git a/.arcconfig b/.arcconfig
new file mode 100644 (file)
index 0000000..20d53ee
--- /dev/null
@@ -0,0 +1,4 @@
+{
+  "phabricator.uri" : "https://phabricator.kde.org/project/profile/34/",
+  "history.immutable" : true
+}
diff --git a/.emacs-dirvars b/.emacs-dirvars
new file mode 100644 (file)
index 0000000..d8419e5
--- /dev/null
@@ -0,0 +1,14 @@
+;; -*- emacs-lisp -*-
+;;
+;; This file is processed by the dirvars emacs package.  Each variable
+;; setting below is performed when this dirvars file is loaded.
+;;
+c-block-comment-prefix: ""
+indent-tabs-mode: nil
+tab-width: 8
+c-basic-offset: 2
+evaluate: (c-set-offset 'innamespace '0)
+evaluate: (c-set-offset 'access-label '0)
+;evaluate: (c-set-offset 'topmost-intro '+)
+evaluate: (c-set-offset 'inline-open '+)
+kdab-qt-version: 4
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..cd88be4
--- /dev/null
@@ -0,0 +1,24 @@
+# Ignore the following files
+*~
+*.[oa]
+*.kdev4
+.kdev_include_paths
+.swp.*
+.*.swp
+*.kate-swp
+Makefile
+*.moc
+*.moc.cpp
+autom4te.cache
+*.diff
+svn-commit.tmp
+svn-commit.*.tmp
+*.kdevelop.pcs
+*.kdev4
+svnmerge-commit-message.txt
+avail
+Doxyfile
+*.user
+/build/
+CMakeLists.txt.user*
+
diff --git a/.krazy b/.krazy
new file mode 100644 (file)
index 0000000..945ea5a
--- /dev/null
+++ b/.krazy
@@ -0,0 +1,6 @@
+SKIP /libkdepim-copy/
+EXTRA defines,kdebug,null,qenums,tipsandthis
+STRICT super
+
+#kresources is pretty much dead
+IGNORESUBS kresources
diff --git a/.reviewboardrc b/.reviewboardrc
new file mode 100644 (file)
index 0000000..4d23bcc
--- /dev/null
@@ -0,0 +1,5 @@
+REVIEWBOARD_URL = "https://git.reviewboard.kde.org"
+TARGET_GROUPS = "kdepim"
+REPOSITORY = "kdepim-runtime"
+TARGET_PEOPLE = "mlaurent"
+BRANCH = "master"
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644 (file)
index 0000000..9280777
--- /dev/null
@@ -0,0 +1,154 @@
+cmake_minimum_required(VERSION 2.8.12)
+
+project(kdepim-runtime)
+
+############### KDEPIM-Runtime version ################
+# KDEPIM_RUNTIME_VERSION
+# Version scheme: "x.y.z build".
+#
+# x is the version number.
+# y is the major release number.
+# z is the minor release number.
+#
+# "x.y.z" follow the kdelibs version kdepim is released with.
+#
+# If "z" is 0, it the version is "x.y"
+#
+# KDEPIM_RUNTIME_DEV_VERSION
+# is empty for final versions. For development versions "build" is
+# something like "pre", "alpha1", "alpha2", "beta1", "beta2", "rc1", "rc2".
+#
+# Examples in chronological order:
+#
+#    3.0
+#    3.0.1
+#    3.1 alpha1
+#    3.1 beta1
+#    3.1 beta2
+#    3.1 rc1
+#    3.1
+#    3.1.1
+#    3.2 pre
+#    3.2 alpha1
+
+if(NOT DEFINED KDEPIM_RUNTIME_DEV_VERSION)
+    set(KDEPIM_RUNTIME_DEV_VERSION "")
+endif()
+
+set(KDEPIM_RUNTIME_VERSION_NUMBER "5.2.2")
+set(KDEPIM_RUNTIME_VERSION "${KDEPIM_RUNTIME_VERSION_NUMBER}${KDEPIM_RUNTIME_DEV_VERSION}")
+
+configure_file(kdepim-runtime-version.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/kdepim-runtime-version.h @ONLY)
+
+find_package(ECM 5.19 REQUIRED NO_MODULE)
+set(CMAKE_MODULE_PATH ${kdepim-runtime_SOURCE_DIR}/cmake/modules ${ECM_MODULE_PATH})
+
+include(ECMPackageConfigHelpers)
+include(ECMSetupVersion)
+include(FeatureSummary)
+include(KDEInstallDirs)
+include(KDECMakeSettings)
+include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE)
+include(ECMInstallIcons)
+include(ECMQtDeclareLoggingCategory)
+
+set(KF5_VERSION "5.19")
+set(QT_REQUIRED_VERSION "5.5.0")
+
+set(KDEPIMLIBS_LIB_VERSION "5.2.0")
+
+set(KDEPIMRUNTIME_LIB_VERSION "${KDEPIM_RUNTIME_VERSION_NUMBER}")
+set(KDEPIMRUNTIME_LIB_SOVERSION "5")
+set(AKONADI_VERSION "5.2.0")
+
+set(KCONTACTS_LIB_VERSION "5.2.0")
+set(KCALENDARCORE_LIB_VERSION "5.2.0")
+set(IDENTITYMANAGEMENT_LIB_VERSION "5.2.0")
+set(KMAILTRANSPORT_LIB_VERSION "5.2.0")
+set(CALENDARUTILS_LIB_VERSION "5.2.0")
+set(KIMAP_LIB_VERSION "5.2.0")
+set(KMBOX_LIB_VERSION "5.2.0")
+set(AKONADICALENDAR_LIB_VERSION "5.2.0")
+set(SYNDICATION_LIB_VERSION "5.2.0")
+set(KONTACTINTERFACE_LIB_VERSION "5.2.0")
+set(AKONADIKALARM_LIB_VERSION "5.2.0")
+set(KMIME_LIB_VERSION "5.2.0")
+set(XMLRPCCLIENT_LIB_VERSION "5.2.0")
+set(KCONTACTS_LIB_VERSION "5.2.0")
+set(AKONADIMIME_LIB_VERSION "5.2.0")
+set(AKONADICONTACT_LIB_VERSION "5.2.0")
+set(AKONADINOTE_LIB_VERSION "5.2.0")
+set(AKONADISOCIALUTIL_LIB_VERSION "5.2.0")
+set(KPIMTEXTEDIT_LIB_VERSION "5.2.0")
+
+set( SHARED_MIME_INFO_MINIMUM_VERSION "0.40" )
+find_package( SharedMimeInfo REQUIRED )
+
+
+find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED Network Widgets Test XmlPatterns DBus)
+
+# QT5 package
+find_package(Qt5WebKitWidgets ${QT_REQUIRED_VERSION} REQUIRED NO_MODULE)
+
+find_package(Qt5 OPTIONAL_COMPONENTS TextToSpeech)
+if (NOT Qt5TextToSpeech_FOUND)
+    message(STATUS "Qt5TextToSpeech not found, speech feature will be disabled")
+else()
+    add_definitions(-DHAVE_SPEECH)
+endif()
+
+
+# KF5 package
+find_package(KF5KDELibs4Support ${KF5_VERSION} CONFIG REQUIRED)
+find_package(KF5Config ${KF5_VERSION} CONFIG REQUIRED)
+find_package(KF5ConfigWidgets ${KF5_VERSION} CONFIG REQUIRED)
+find_package(KF5NotifyConfig ${KF5_VERSION} CONFIG REQUIRED)
+find_package(KF5KIO ${KF5_VERSION} CONFIG REQUIRED)
+find_package(KF5ItemModels ${KF5_VERSION} CONFIG REQUIRED)
+find_package(KF5Kross ${KF5_VERSION} CONFIG REQUIRED)
+find_package(KF5Codecs ${KF5_VERSION} CONFIG REQUIRED)
+find_package(KF5WindowSystem ${KF5_VERSION} CONFIG REQUIRED)
+find_packagE(KF5TextWidgets ${KF5_VERSION} CONFIG REQUIRED) # for KPluralHandlingSpinBox
+
+# KdepimLibs package
+find_package(KF5Akonadi ${AKONADI_VERSION} CONFIG REQUIRED)
+find_package(KF5Mime ${KMIME_LIB_VERSION} CONFIG REQUIRED)
+find_package(KF5AkonadiMime ${AKONADIMIME_LIB_VERSION} CONFIG REQUIRED)
+find_package(KF5MailTransport ${KMAILTRANSPORT_LIB_VERSION} CONFIG REQUIRED)
+find_package(KF5IdentityManagement ${IDENTITYMANAGEMENT_LIB_VERSION} CONFIG REQUIRED)
+find_package(KF5AkonadiContact ${AKONADICONTACT_LIB_VERSION} CONFIG REQUIRED)
+find_package(KF5Contacts ${KCONTACTS_LIB_VERSION} CONFIG REQUIRED)
+find_package(KF5AlarmCalendar ${AKONADIKALARM_LIB_VERSION} CONFIG REQUIRED)
+find_package(KF5CalendarCore ${KCALENDARCORE_LIB_VERSION} CONFIG REQUIRED)
+find_package(KF5CalendarUtils ${CALENDARUTILS_LIB_VERSION} CONFIG REQUIRED)
+find_package(KF5Mbox ${KMBOX_LIB_VERSION} CONFIG REQUIRED)
+find_package(KF5PimTextEdit ${KPIMTEXTEDIT_LIB_VERSION} CONFIG REQUIRED)
+find_package(KF5IMAP ${KIMAP_LIB_VERSION} CONFIG REQUIRED)
+find_package(KF5Syndication ${SYNDICATION_LIB_VERSION} CONFIG REQUIRED)
+find_package(KF5AkonadiNotes ${AKONADINOTE_LIB_VERSION} CONFIG REQUIRED)
+find_package(KF5AkonadiSocialUtils ${AKONADISOCIALUTIL_LIB_VERSION} CONFIG REQUIRED)
+
+option(KDEPIM_RUN_ISOLATED_TESTS "Run the isolated tests." FALSE)
+
+add_definitions( -DQT_NO_CAST_FROM_ASCII )
+add_definitions( -DQT_NO_CAST_TO_ASCII )
+
+add_subdirectory(resources)
+add_subdirectory(agents)
+add_subdirectory(plugins)
+add_subdirectory(defaultsetup)
+add_subdirectory(kioslave)
+add_subdirectory(migration)
+
+
+## install the MIME type spec file for KDEPIM specific MIME types
+install(FILES kdepim-mime.xml DESTINATION ${KDE_INSTALL_MIMEDIR})
+update_xdg_mimetypes(${KDE_INSTALL_MIMEDIR})
+
+
+install( FILES kdepim-runtime.categories DESTINATION ${KDE_INSTALL_CONFDIR} )
+
+feature_summary(WHAT ALL
+                INCLUDE_QUIET_PACKAGES
+                FATAL_ON_MISSING_REQUIRED_PACKAGES
+)
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..5185fd3
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,346 @@
+NOTE! The GPL below is copyrighted by the Free Software Foundation, but
+the instance of code that it refers to (the kde programs) are copyrighted
+by the authors who actually wrote it.
+
+---------------------------------------------------------------------------
+                       
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+               51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+\f
+                   GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+\f
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+\f
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+\f
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                           NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) 19yy  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) 19yy name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/COPYING.LIB b/COPYING.LIB
new file mode 100644 (file)
index 0000000..2d2d780
--- /dev/null
@@ -0,0 +1,510 @@
+
+                  GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+       51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations
+below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+\f
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it
+becomes a de-facto standard.  To achieve this, non-free programs must
+be allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+\f
+                  GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control
+compilation and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+\f
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+\f
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+\f
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at least
+    three years, to give the same user the materials specified in
+    Subsection 6a, above, for a charge no more than the cost of
+    performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+\f
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+\f
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply, and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License
+may add an explicit geographical distribution limitation excluding those
+countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+\f
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                            NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+\f
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms
+of the ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.
+It is safest to attach them to the start of each source file to most
+effectively convey the exclusion of warranty; and each file should
+have at least the "copyright" line and a pointer to where the full
+notice is found.
+
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or
+your school, if any, to sign a "copyright disclaimer" for the library,
+if necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James
+  Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
+
+
diff --git a/CTestConfig.cmake b/CTestConfig.cmake
new file mode 100644 (file)
index 0000000..e8bc4ba
--- /dev/null
@@ -0,0 +1,13 @@
+## This file should be placed in the root directory of your project.
+## Then modify the CMakeLists.txt file in the root directory of your
+## project to incorporate the testing dashboard.
+## # The following are required to uses Dart and the Cdash dashboard
+##   ENABLE_TESTING()
+##   INCLUDE(CTest)
+set(CTEST_PROJECT_NAME "kdepim-runtime")
+set(CTEST_NIGHTLY_START_TIME "20:00:00 CET")
+
+set(CTEST_DROP_METHOD "http")
+set(CTEST_DROP_SITE "my.cdash.org")
+set(CTEST_DROP_LOCATION "/submit.php?project=kdepim-runtime")
+set(CTEST_DROP_SITE_CDASH TRUE)
diff --git a/CTestCustom.cmake b/CTestCustom.cmake
new file mode 100644 (file)
index 0000000..3385248
--- /dev/null
@@ -0,0 +1,31 @@
+# This file contains all the specific settings that will be used
+# when running 'make Experimental'
+
+# Change the maximum warnings that will be displayed
+# on the report page (default 50)
+set(CTEST_CUSTOM_MAXIMUM_NUMBER_OF_WARNINGS 2000)
+
+# Warnings that will be ignored
+set(CTEST_CUSTOM_WARNING_EXCEPTION
+  
+  "too big, try a different debug format"
+  "qlist.h.*increases required alignment of target type"
+  "qmap.h.*increases required alignment of target type"
+  "qhash.h.*increases required alignment of target type"
+  ".*warning.* is deprecated"
+  )
+
+# Errors that will be ignored
+set(CTEST_CUSTOM_ERROR_EXCEPTION
+  
+  "ICECC"
+  "Segmentation fault"
+  "Error 1 (ignored)"
+  "invoking macro kDebug argument 1"
+  "GConf Error"
+  "Client failed to connect to the D-BUS daemon"
+  "Failed to connect to socket"
+  )
+
+# No coverage for these files
+set(CTEST_CUSTOM_COVERAGE_EXCLUDE ".moc$" "moc_" "ui_" "ontologies")
diff --git a/MIGRATE-CONFIG-APPS b/MIGRATE-CONFIG-APPS
new file mode 100644 (file)
index 0000000..805013d
--- /dev/null
@@ -0,0 +1,3 @@
+Migrate configrc done:
+- agent/newmailnotifieragent: akonadi_newmailnotifier_agentrc, akonadi_newmailnotifier_agent.notifyrc
+- agent/maildispatcher: maildispatcheragentrc, akonadi_maildispatcher_agent.notifyrc
diff --git a/Mainpage.dox b/Mainpage.dox
new file mode 100644 (file)
index 0000000..2b1bd76
--- /dev/null
@@ -0,0 +1,105 @@
+/**
+\mainpage Akonadi, the KDE PIM storage framework
+
+These pages are a combination of design and api documentation. If you are
+looking for general information go to the Overview section.
+For detailed information and/or api-dox on any of the packages go to the
+package main page, either from the menu on the left or from the Building
+blocks section below.
+
+
+\section akonadi_overview Overview
+
+- Design and Concepts
+  - \ref akonadi_design_communication
+- \ref akonadi_usage
+- <a href="http://pim.kde.org/akonadi">Website</a>
+- <a href="http://techbase.kde.org/index.php?title=Projects/PIM/Akonadi">Wiki</a>
+- \ref akonadi_todos
+
+\section akonadi_building_blocks Building Blocks
+
+- Domain-specific libraries:
+  - <a href="../../kdepimlibs-apidocs/kabc/html/index.html">Contacts (KABC)</a>
+  - <a href="../../kdepimlibs-apidocs/kmime/html/index.html">MIME Messages (KMime)</a>
+  - <a href="../../kdepimlibs-apidocs/kcal/html/index.html">Events, todo items and journal entries (KCal)</a>
+  - <a href="../../kdepimlibs-apidocs/akonadi/html/index.html">Core library (libakonadi)</a>
+
+\authors Tobias König <tokoe@kde.org>, Volker Krause <vkrause@kde.org>
+
+\licenses \lgpl
+*/
+
+
+/**
+\page akonadi_design_communication Communication Schemas
+
+\section akonadi_design_communication_search Search
+
+The sequence diagrams below show how general communication is done:
+
+<div id='akonadi_client_search_small.png' onClick="document.getElementById('akonadi_client_search_small.png').style.display='none';document.getElementById(' akonadi_client_search.png').style.display='inline';" title="Click to enlarge" >
+  \image html akonadi_client_search_small.png  "Akonadi Communication Schema"
+ </div>
+ <div id=' akonadi_client_search.png' onClick="document.getElementById(' akonadi_client_search.png').style.display='none';document.getElementById('akonadi_client_search_small.png').style.display='inline';" style="display:none" >
+  \image html  akonadi_client_search.png  "Akonadi Communication Schema"
+ </div>
+
+\image latex akonadi_client_search.eps "Akonadi Communication Schema" height=5cm
+
+The item search request is probably the call which is used most often
+by the clients (components or applications). This call enables the client
+to search for a list of items of a given mime type which match a
+given search criterion.
+
+In this case the client will contact the SearchProvider responsible for
+the mime type, in order to retrieve the list of matching UIDs. The SearchProvider
+already has a list of all available items of this mime type in its memory, so it
+can search fast and use indices for optimization.
+
+To communicate mime type constraints in FETCH and LIST and their responses the
+IMAP flags mechanism is used. Unknown flags should be ignored by non-Akonadi
+IMAP clients, which keeps compatibility with mutt and regular KMail.
+
+Examples:
+- List
+\verbatim
+0x8053c68 8 LIST "" "res1/foo/%"
+0x8053c68 * LIST (\MimeTypes[text/calendar,directory/inode]) "/" "res1/foo/bar"
+\endverbatim
+- Fetch
+\verbatim
+0x8056310 7 UID FETCH 22 (UID RFC822.SIZE FLAGS BODY.PEEK[])
+0x8056310 * 1 FETCH (FLAGS (\Seen \MimeTypes[message/rfc822]) RFC822 {2450} From: Till Adam To: ...
+\endverbatim
+
+\section akonadi_design_communication_agent Agent Handling
+<div id=' akonadi_agent_handling_small.png' onClick="document.getElementById(' akonadi_agent_handling_small.png').style.display='none';document.getElementById(' akonadi_agent_handling.png').style.display='inline';" title="Click to enlarge" >
+  \image html  akonadi_agent_handling_small.png  "Akonadi Agent Handling"
+ </div>
+ <div id=' akonadi_agent_handling.png' onClick="document.getElementById(' akonadi_agent_handling.png').style.display='none';document.getElementById(' akonadi_agent_handling_small.png').style.display='inline';" style="display:none" >
+  \image html  akonadi_agent_handling.png  "Akonadi Agent Handling"
+ </div>
+
+\image latex akonadi_agent_handling.eps "Akonadi Agent Handling" height=4cm
+*/
+
+/**
+\page akonadi_overview_uml Akonadi Overview
+
+This overview does not show a complete class or collaboration diagram, it is rather meant to show the
+big picture.
+
+<div id=' akonadi_overview_uml_small.png' onClick="document.getElementById(' akonadi_overview_uml_small.png').style.display='none';document.getElementById(' akonadi_overview_uml.png').style.display='inline';" title="Click to enlarge" >
+  \image html  akonadi_overview_uml_small.png  "Akonadi Overview"
+ </div>
+ <div id=' akonadi_overview_uml.png' onClick="document.getElementById(' akonadi_overview_uml.png').style.display='none';document.getElementById(' akonadi_overview_uml_small.png').style.display='inline';" style="display:none" >
+  \image html  akonadi_overview_uml.png  "Akonadi Overview"
+ </div>
+
+
+*/
+
+
+// DOXYGEN_NAME=kdepim-runtime
+// DOXYGEN_ENABLE=YES
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..646778a
--- /dev/null
+++ b/README
@@ -0,0 +1,3 @@
+The KDE project kdepim-runtime contains the Akonadi resources from kdepim which can be used without the applications in kdepim.
+
+Here is the online code-repostitory of kdepim-runtime: http://quickgit.kde.org/?p=kdepim-runtime.git&a=summary
diff --git a/agents/.krazy b/agents/.krazy
new file mode 100644 (file)
index 0000000..4152f9b
--- /dev/null
@@ -0,0 +1 @@
+SKIP /ontologies/
diff --git a/agents/CMakeLists.txt b/agents/CMakeLists.txt
new file mode 100644 (file)
index 0000000..a4586ce
--- /dev/null
@@ -0,0 +1,10 @@
+project(agents)
+
+add_definitions( -DQT_NO_CAST_FROM_ASCII )
+add_definitions( -DQT_NO_CAST_TO_ASCII )
+
+add_subdirectory( maildispatcher )
+add_subdirectory( newmailnotifier )
+add_subdirectory( migration )
+add_subdirectory( invitations )
+
diff --git a/agents/Info.plist.template b/agents/Info.plist.template
new file mode 100644 (file)
index 0000000..c39ddb9
--- /dev/null
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+    <key>CFBundleDevelopmentRegion</key>
+    <string>English</string>
+    <key>CFBundleExecutable</key>
+    <string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
+    <key>CFBundleGetInfoString</key>
+    <string>${MACOSX_BUNDLE_INFO_STRING}</string>
+    <key>CFBundleIconFile</key>
+    <string>${MACOSX_BUNDLE_ICON_FILE}</string>
+    <key>CFBundleIdentifier</key>
+    <string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
+    <key>CFBundleInfoDictionaryVersion</key>
+    <string>6.0</string>
+    <key>CFBundleLongVersionString</key>
+    <string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string>
+    <key>CFBundleName</key>
+    <string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
+    <key>CFBundlePackageType</key>
+    <string>APPL</string>
+    <key>CFBundleShortVersionString</key>
+    <string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
+    <key>CFBundleVersion</key>
+    <string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
+    <key>CSResourcesFileMapped</key>
+    <true/>
+    <key>LSRequiresCarbon</key>
+    <true/>
+    <key>LSUIElement</key>
+    <string>1</string>
+    <key>NSHumanReadableCopyright</key>
+    <string>${MACOSX_BUNDLE_COPYRIGHT}</string>
+</dict>
+</plist>
diff --git a/agents/Mainpage.dox b/agents/Mainpage.dox
new file mode 100644 (file)
index 0000000..c99c6ac
--- /dev/null
@@ -0,0 +1,2 @@
+// DOXYGEN_NAME=Akonadi Agents
+// DOXYGEN_ENABLE=YES
diff --git a/agents/cmake/FindXsltproc.cmake b/agents/cmake/FindXsltproc.cmake
new file mode 100644 (file)
index 0000000..45b46cf
--- /dev/null
@@ -0,0 +1,32 @@
+# Find xsltproc executable and provide a macro to generate D-Bus interfaces.
+#
+# The following variables are defined :
+# XSLTPROC_EXECUTABLE - path to the xsltproc executable
+# Xsltproc_FOUND - true if the program was found
+#
+find_program(XSLTPROC_EXECUTABLE xsltproc DOC "Path to the xsltproc executable")
+mark_as_advanced(XSLTPROC_EXECUTABLE)
+
+if(XSLTPROC_EXECUTABLE)
+  set(Xsltproc_FOUND TRUE)
+
+  # We depend on kdepimlibs, make sure it's found
+  if(NOT DEFINED KF5Akonadi_DATA_DIR)
+    find_package(KF5Akonadi REQUIRED)
+  endif()
+
+
+  # Macro to generate a D-Bus interface description from a KConfigXT file
+  macro(kcfg_generate_dbus_interface _kcfg _name)
+    add_custom_command(
+      OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${_name}.xml
+      COMMAND ${XSLTPROC_EXECUTABLE} --stringparam interfaceName ${_name}
+      ${KF5Akonadi_DATA_DIR}/kcfg2dbus.xsl
+      ${_kcfg}
+      > ${CMAKE_CURRENT_BINARY_DIR}/${_name}.xml
+      DEPENDS ${KF5Akonadi_DATA_DIR}/kcfg2dbus.xsl
+      ${_kcfg}
+      )
+  endmacro()
+endif()
+
diff --git a/agents/invitations/CMakeLists.txt b/agents/invitations/CMakeLists.txt
new file mode 100644 (file)
index 0000000..be62d37
--- /dev/null
@@ -0,0 +1,28 @@
+
+
+set( invitationsagent_SRCS
+  invitationsagent.cpp
+  incidenceattribute.cpp
+)
+
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_invitations_agent\")
+
+add_executable(akonadi_invitations_agent ${invitationsagent_SRCS})
+
+target_link_libraries(akonadi_invitations_agent
+  KF5::AkonadiCore
+  KF5::AkonadiMime
+  KF5::Mime
+  KF5::CalendarCore
+  KF5::AkonadiAgentBase
+)
+
+if( APPLE )
+  set_target_properties(akonadi_invitations_agent PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../Info.plist.template)
+  set_target_properties(akonadi_invitations_agent PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.invitationsagent")
+  set_target_properties(akonadi_invitations_agent PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi Invitations Calendar")
+endif ()
+
+
+install(TARGETS akonadi_invitations_agent ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
+install(FILES invitationsagent.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents")
diff --git a/agents/invitations/Messages.sh b/agents/invitations/Messages.sh
new file mode 100755 (executable)
index 0000000..6f0436a
--- /dev/null
@@ -0,0 +1,3 @@
+#! /bin/sh
+
+$XGETTEXT *.cpp -o $podir/akonadi_invitations_agent.pot
diff --git a/agents/invitations/incidenceattribute.cpp b/agents/invitations/incidenceattribute.cpp
new file mode 100644 (file)
index 0000000..d3c277e
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+    Copyright (c) 2009 Sebastian Sauer <sebsauer@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "incidenceattribute.h"
+
+#include <QtCore/QString>
+#include <QtCore/QTextStream>
+
+using namespace Akonadi;
+
+class IncidenceAttribute::Private
+{
+public:
+    QString status;
+    Akonadi::Item::Id referenceId;
+
+    explicit Private() : referenceId(-1) {}
+};
+
+IncidenceAttribute::IncidenceAttribute()
+    : Attribute(), d(new Private)
+{
+}
+
+IncidenceAttribute::~IncidenceAttribute()
+{
+    delete d;
+}
+
+QByteArray IncidenceAttribute::type() const
+{
+    static const QByteArray sType("incidence");
+    return sType;
+}
+
+Attribute *IncidenceAttribute::clone() const
+{
+    IncidenceAttribute *other = new IncidenceAttribute;
+    return other;
+}
+
+QByteArray IncidenceAttribute::serialized() const
+{
+    QString data;
+    QTextStream out(&data);
+    out << d->status;
+    out << d->referenceId;
+    return data.toUtf8();
+}
+
+void IncidenceAttribute::deserialize(const QByteArray &data)
+{
+    QString s(QString::fromUtf8(data));
+    QTextStream in(&s);
+    in >> d->status;
+    in >> d->referenceId;
+}
+
+QString IncidenceAttribute::status() const
+{
+    return d->status;
+}
+
+void IncidenceAttribute::setStatus(const QString &newstatus) const
+{
+    d->status = newstatus;
+}
+
+Akonadi::Item::Id IncidenceAttribute::reference() const
+{
+    return d->referenceId;
+}
+
+void IncidenceAttribute::setReference(Akonadi::Item::Id id)
+{
+    d->referenceId = id;
+}
diff --git a/agents/invitations/incidenceattribute.h b/agents/invitations/incidenceattribute.h
new file mode 100644 (file)
index 0000000..21ccc0d
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+    Copyright (c) 2009 Sebastian Sauer <sebsauer@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef INCIDENCEATTRIBUTE_H
+#define INCIDENCEATTRIBUTE_H
+
+#include <item.h>
+#include <attribute.h>
+
+namespace Akonadi
+{
+
+class IncidenceAttribute : public Akonadi::Attribute
+{
+public:
+    explicit IncidenceAttribute();
+    ~IncidenceAttribute();
+
+    QByteArray type() const Q_DECL_OVERRIDE;
+    Attribute *clone() const Q_DECL_OVERRIDE;
+
+    QByteArray serialized() const Q_DECL_OVERRIDE;
+    void deserialize(const QByteArray &data) Q_DECL_OVERRIDE;
+
+    /**
+     * The status the invitation is in.
+     *
+     * One of;
+     * "new", "accepted", "tentative", "counter", "cancel", "reply", "delegated"
+     */
+    QString status() const;
+    void setStatus(const QString &newstatus) const;
+
+    /**
+     * The referenced item. This is used e.g. in the invitationagent to
+     * let users know where the original mail message is.
+     */
+    Akonadi::Item::Id reference() const;
+    void setReference(Akonadi::Item::Id id);
+
+private:
+    class Private;
+    Private *const d;
+};
+
+}
+
+#endif
diff --git a/agents/invitations/invitationsagent.cpp b/agents/invitations/invitationsagent.cpp
new file mode 100644 (file)
index 0000000..9f0d478
--- /dev/null
@@ -0,0 +1,569 @@
+/*
+    Copyright 2009 Sebastian Sauer <sebsauer@kdab.net>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "invitationsagent.h"
+
+#include "incidenceattribute.h"
+
+#include <AgentInstance>
+#include <AgentInstanceCreateJob>
+#include <AgentManager>
+#include <ChangeRecorder>
+#include <Collection>
+#include <CollectionCreateJob>
+#include <CollectionFetchJob>
+#include <CollectionFetchScope>
+#include <ItemFetchJob>
+#include <ItemFetchScope>
+#include <ItemModifyJob>
+#include <Akonadi/KMime/MessageFlags>
+#include <resourcesynchronizationjob.h>
+#include <specialcollections.h>
+#include <specialcollectionsrequestjob.h>
+
+#include <KCalCore/Event>
+#include <KCalCore/ICalFormat>
+#include <KCalCore/Incidence>
+#include <KCalCore/Journal>
+#include <KCalCore/Todo>
+#include <KConfig>
+#include <KConfigSkeleton>
+#include <QDebug>
+#include <KJob>
+#include <KLocalizedString>
+#include <KMime/Content>
+#include <KMime/Message>
+#include <KSystemTimeZones>
+
+#include <QtCore/QFileInfo>
+#include <QtCore/QTimer>
+#include <QtDBus/QDBusInterface>
+#include <QtDBus/QDBusReply>
+#include <QStandardPaths>
+
+using namespace Akonadi;
+
+class InvitationsCollectionRequestJob : public SpecialCollectionsRequestJob
+{
+public:
+    InvitationsCollectionRequestJob(SpecialCollections *collection, InvitationsAgent *agent)
+        : SpecialCollectionsRequestJob(collection, agent)
+    {
+        setDefaultResourceType(QStringLiteral("akonadi_ical_resource"));
+
+        QVariantMap options;
+        options.insert(QStringLiteral("Path"), QString(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + QStringLiteral("akonadi_invitations")));
+        options.insert(QStringLiteral("Name"), i18n("Invitations"));
+        setDefaultResourceOptions(options);
+
+        QMap<QByteArray, QString> displayNameMap;
+        displayNameMap.insert("invitations", i18n("Invitations"));
+        setTypes(displayNameMap.keys());
+        setNameForTypeMap(displayNameMap);
+
+        QMap<QByteArray, QString> iconNameMap;
+        iconNameMap.insert("invitations", QStringLiteral("folder"));
+        setIconForTypeMap(iconNameMap);
+    }
+
+    virtual ~InvitationsCollectionRequestJob()
+    {
+    }
+};
+
+class  InvitationsCollection : public SpecialCollections
+{
+public:
+
+    class Settings : public KCoreConfigSkeleton
+    {
+    public:
+        Settings()
+        {
+            setCurrentGroup(QStringLiteral("Invitations"));
+            addItemString(QStringLiteral("DefaultResourceId"), m_id, QString());
+        }
+
+        virtual ~Settings()
+        {
+        }
+
+    private:
+        QString m_id;
+    };
+
+    InvitationsCollection(InvitationsAgent *agent)
+        : Akonadi::SpecialCollections(new Settings), m_agent(agent), sInvitationsType("invitations")
+    {
+    }
+
+    virtual ~InvitationsCollection()
+    {
+    }
+
+    void clear()
+    {
+        m_collection = Collection();
+    }
+
+    bool hasDefaultCollection() const
+    {
+        return SpecialCollections::hasDefaultCollection(sInvitationsType);
+    }
+
+    Collection defaultCollection() const
+    {
+        if (!m_collection.isValid()) {
+            m_collection = SpecialCollections::defaultCollection(sInvitationsType);
+        }
+
+        return m_collection;
+    }
+
+    void registerDefaultCollection()
+    {
+        defaultCollection();
+        registerCollection(sInvitationsType, m_collection);
+    }
+
+    SpecialCollectionsRequestJob *reguestJob() const
+    {
+        InvitationsCollectionRequestJob *job = new InvitationsCollectionRequestJob(const_cast<InvitationsCollection *>(this),
+                m_agent);
+        job->requestDefaultCollection(sInvitationsType);
+
+        return job;
+    }
+
+private:
+    InvitationsAgent *m_agent;
+    const QByteArray sInvitationsType;
+    mutable Collection m_collection;
+};
+
+InvitationsAgentItem::InvitationsAgentItem(InvitationsAgent *agent, const Item &originalItem)
+    : QObject(agent), m_agent(agent), m_originalItem(originalItem)
+{
+}
+
+InvitationsAgentItem::~InvitationsAgentItem()
+{
+}
+
+void InvitationsAgentItem::add(const Item &item)
+{
+    qDebug();
+    const Collection collection = m_agent->collection();
+    Q_ASSERT(collection.isValid());
+
+    ItemCreateJob *job = new ItemCreateJob(item, collection, this);
+    connect(job, &InvitationsCollectionRequestJob::result, this, &InvitationsAgentItem::createItemResult);
+
+    m_jobs << job;
+
+    job->start();
+}
+
+void InvitationsAgentItem::createItemResult(KJob *job)
+{
+    ItemCreateJob *createJob = qobject_cast<ItemCreateJob *>(job);
+    m_jobs.removeAll(createJob);
+    if (createJob->error()) {
+        qWarning() << "Failed to create new Item in invitations collection." << createJob->errorText();
+        return;
+    }
+
+    if (!m_jobs.isEmpty()) {
+        return;
+    }
+
+    ItemFetchJob *fetchJob = new ItemFetchJob(m_originalItem, this);
+    connect(fetchJob, &ItemFetchJob::result, this, &InvitationsAgentItem::fetchItemDone);
+    fetchJob->start();
+}
+
+void InvitationsAgentItem::fetchItemDone(KJob *job)
+{
+    if (job->error()) {
+        qWarning() << "Failed to fetch Item in invitations collection." << job->errorText();
+        return;
+    }
+
+    ItemFetchJob *fetchJob = qobject_cast<ItemFetchJob *>(job);
+    Q_ASSERT(fetchJob->items().count() == 1);
+
+    Item modifiedItem = fetchJob->items().at(0);
+    Q_ASSERT(modifiedItem.isValid());
+
+    modifiedItem.setFlag(Akonadi::MessageFlags::HasInvitation);
+    ItemModifyJob *modifyJob = new ItemModifyJob(modifiedItem, this);
+    connect(modifyJob, &ItemModifyJob::result, this, &InvitationsAgentItem::modifyItemDone);
+    modifyJob->start();
+}
+
+void InvitationsAgentItem::modifyItemDone(KJob *job)
+{
+    if (job->error()) {
+        qWarning() << "Failed to modify Item in invitations collection." << job->errorText();
+        return;
+    }
+
+    //ItemModifyJob *mj = qobject_cast<ItemModifyJob*>( job );
+    //qDebug()<<"Job successful done.";
+}
+
+InvitationsAgent::InvitationsAgent(const QString &id)
+    : AgentBase(id), AgentBase::ObserverV2()
+    , m_invitationsCollection(new InvitationsCollection(this))
+{
+    qDebug();
+
+    changeRecorder()->setChangeRecordingEnabled(false);   // behave like Monitor
+    changeRecorder()->itemFetchScope().fetchFullPayload();
+    changeRecorder()->setMimeTypeMonitored(QStringLiteral("message/rfc822"), true);
+    //changeRecorder()->setCollectionMonitored( Collection::root(), true );
+
+    connect(this, &InvitationsAgent::reloadConfiguration, this, &InvitationsAgent::initStart);
+    QTimer::singleShot(0, this, &InvitationsAgent::initStart);
+}
+
+InvitationsAgent::~InvitationsAgent()
+{
+    delete m_invitationsCollection;
+}
+
+void InvitationsAgent::initStart()
+{
+    qDebug();
+
+    m_invitationsCollection->clear();
+    if (m_invitationsCollection->hasDefaultCollection()) {
+        initDone();
+    } else {
+        SpecialCollectionsRequestJob *job = m_invitationsCollection->reguestJob();
+        connect(job, &InvitationsCollectionRequestJob::result, this, &InvitationsAgent::initDone);
+        job->start();
+    }
+
+    /*
+    KConfig config( "akonadi_invitations_agent" );
+    KConfigGroup group = config.group( "General" );
+    m_resourceId = group.readEntry( "DefaultCalendarAgent", "default_ical_resource" );
+    newAgentCreated = false;
+    m_invitations = Akonadi::Collection();
+    AgentInstance resource = AgentManager::self()->instance( m_resourceId );
+    if ( resource.isValid() ) {
+     Q_EMIT status( AgentBase::Running, i18n( "Reading..." ) );
+      QMetaObject::invokeMethod( this, "createAgentResult", Qt::QueuedConnection );
+    } else {
+     Q_EMIT status( AgentBase::Running, i18n( "Creating..." ) );
+      AgentType type = AgentManager::self()->type( QLatin1String( "akonadi_ical_resource" ) );
+      AgentInstanceCreateJob *job = new AgentInstanceCreateJob( type, this );
+      connect(job, &InvitationsCollectionRequestJob::result, this, &InvitationsAgent::createAgentResult);
+      job->start();
+    }
+    */
+}
+
+void InvitationsAgent::initDone(KJob *job)
+{
+    if (job) {
+        if (job->error()) {
+            qWarning() << "Failed to request default collection:" << job->errorText();
+            return;
+        }
+
+        m_invitationsCollection->registerDefaultCollection();
+    }
+
+    Q_ASSERT(m_invitationsCollection->defaultCollection().isValid());
+    Q_EMIT status(AgentBase::Idle, i18n("Ready to dispatch invitations"));
+}
+
+Collection InvitationsAgent::collection()
+{
+    return m_invitationsCollection->defaultCollection();
+}
+
+#if 0
+KIdentityManagement::IdentityManager *InvitationsAgent::identityManager()
+{
+    if (!m_IdentityManager) {
+        m_IdentityManager = new KIdentityManagement::IdentityManager(true /* readonly */, this);
+    }
+    return m_IdentityManager;
+}
+#endif
+
+void InvitationsAgent::configure(WId windowId)
+{
+    qDebug() << windowId;
+    /*
+    QWidget *parent = QWidget::find( windowId );
+    QDialog *dialog = new QDialog( parent );
+    QVBoxLayout *layout = new QVBoxLayout( dialog->mainWidget() );
+    //layout->addWidget(  );
+    dialog->mainWidget()->setLayout( layout );
+    */
+    initStart(); //reload
+}
+
+#if 0
+void InvitationsAgent::createAgentResult(KJob *job)
+{
+    qDebug();
+    AgentInstance agent;
+    if (job) {
+        if (job->error()) {
+            qWarning() << job->errorString();
+            Q_EMIT status(AgentBase::Broken, i18n("Failed to create resource: %1", job->errorString()));
+            return;
+        }
+
+        AgentInstanceCreateJob *j = static_cast<AgentInstanceCreateJob *>(job);
+        agent = j->instance();
+        agent.setName(i18n("Invitations"));
+        m_resourceId = agent.identifier();
+
+        QDBusInterface conf(QString::fromLatin1("org.freedesktop.Akonadi.Resource.") + m_resourceId,
+                            QString::fromLatin1("/Settings"),
+                            QString::fromLatin1("org.kde.Akonadi.ICal.Settings"));
+        QDBusReply<void> reply = conf.call(QString::fromLatin1("setPath"),
+                                           QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + "akonadi_ical_resource");
+
+        if (!reply.isValid()) {
+            qWarning() << "dbus call failed, m_resourceId=" << m_resourceId;
+            Q_EMIT status(AgentBase::Broken, i18n("Failed to set the directory for invitations via D-Bus"));
+            AgentManager::self()->removeInstance(agent);
+            return;
+        }
+
+        KConfig config("akonadi_invitations_agent");
+        KConfigGroup group = config.group("General");
+        group.writeEntry("DefaultCalendarAgent", m_resourceId);
+
+        newAgentCreated = true;
+        agent.reconfigure();
+    } else {
+        agent = AgentManager::self()->instance(m_resourceId);
+        Q_ASSERT(agent.isValid());
+    }
+
+    ResourceSynchronizationJob *j = new ResourceSynchronizationJob(agent, this);
+    connect(j, &AgentInstanceCreateJob::result, this, &InvitationsAgent::resourceSyncResult);
+    j->start();
+}
+
+void InvitationsAgent::resourceSyncResult(KJob *job)
+{
+    qDebug();
+    if (job->error()) {
+        qWarning() << job->errorString();
+        Q_EMIT status(AgentBase::Broken, i18n("Failed to synchronize collection: %1", job->errorString()));
+        if (newAgentCreated) {
+            AgentManager::self()->removeInstance(AgentManager::self()->instance(m_resourceId));
+        }
+        return;
+    }
+    CollectionFetchJob *fjob = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, this);
+    fjob->fetchScope().setContentMimeTypes(QStringList() << "text/calendar");
+    fjob->fetchScope().setResource(m_resourceId);
+    connect(fjob, &CollectionFetchJob::result, this, &InvitationsAgent::collectionFetchResult);
+    fjob->start();
+}
+
+void InvitationsAgent::collectionFetchResult(KJob *job)
+{
+    qDebug();
+
+    if (job->error()) {
+        qWarning() << job->errorString();
+        Q_EMIT status(AgentBase::Broken, i18n("Failed to fetch collection: %1", job->errorString()));
+        if (newAgentCreated) {
+            AgentManager::self()->removeInstance(AgentManager::self()->instance(m_resourceId));
+        }
+        return;
+    }
+
+    CollectionFetchJob *fj = static_cast<CollectionFetchJob *>(job);
+
+    if (newAgentCreated) {
+        // if the agent was just created then there is exactly one collection already
+        // and we just use that one.
+        Q_ASSERT(fj->collections().count() == 1);
+        m_invitations = fj->collections().at(0);
+        initDone();
+        return;
+    }
+
+    KConfig config("akonadi_invitations_agent");
+    KConfigGroup group = config.group("General");
+    const QString collectionId = group.readEntry("DefaultCalendarCollection", QString());
+    if (!collectionId.isEmpty()) {
+        // look if the collection is still there. It may the case that there exists such
+        // a collection with the defined collectionId but that this is not a valid one
+        // and therefore not in the resultset.
+        const int id = collectionId.toInt();
+        foreach (const Collection &c, fj->collections()) {
+            if (c.id() == id) {
+                m_invitations = c;
+                initDone();
+                return;
+            }
+        }
+    }
+
+    // we need to create a new collection and use that one...
+    Collection c;
+    c.setName("invitations");
+    c.setParent(Collection::root());
+    Q_ASSERT(!m_resourceId.isNull());
+    c.setResource(m_resourceId);
+    c.setContentMimeTypes(QStringList()
+                          << "text/calendar"
+                          << "application/x-vnd.akonadi.calendar.event"
+                          << "application/x-vnd.akonadi.calendar.todo"
+                          << "application/x-vnd.akonadi.calendar.journal"
+                          << "application/x-vnd.akonadi.calendar.freebusy");
+    CollectionCreateJob *cj = new CollectionCreateJob(c, this);
+    connect(cj, &CollectionCreateJob::result, this, &InvitationsAgent::collectionCreateResult);
+    cj->start();
+}
+
+void InvitationsAgent::collectionCreateResult(KJob *job)
+{
+    qDebug();
+    if (job->error()) {
+        qWarning() << job->errorString();
+        Q_EMIT status(AgentBase::Broken, i18n("Failed to create collection: %1", job->errorString()));
+        if (newAgentCreated) {
+            AgentManager::self()->removeInstance(AgentManager::self()->instance(m_resourceId));
+        }
+        return;
+    }
+    CollectionCreateJob *j = static_cast<CollectionCreateJob *>(job);
+    m_invitations = j->collection();
+    initDone();
+}
+#endif
+
+Item InvitationsAgent::handleContent(const QString &vcal,
+                                     const KCalCore::MemoryCalendar::Ptr &calendar,
+                                     const Item &item)
+{
+    KCalCore::ICalFormat format;
+    KCalCore::ScheduleMessage::Ptr message = format.parseScheduleMessage(calendar, vcal);
+    if (!message) {
+        qWarning() << "Invalid invitation:" << vcal;
+        return Item();
+    }
+
+    qDebug() << "id=" << item.id() << "remoteId=" << item.remoteId() << "vcal=" << vcal;
+
+    KCalCore::Incidence::Ptr incidence = message->event().staticCast<KCalCore::Incidence>();
+    Q_ASSERT(incidence);
+
+    IncidenceAttribute *attr = new IncidenceAttribute;
+    attr->setStatus(QStringLiteral("new"));   //TODO
+    //attr->setFrom( message->from()->asUnicodeString() );
+    attr->setReference(item.id());
+
+    Item newItem;
+    newItem.setMimeType(incidence->mimeType());
+    newItem.addAttribute(attr);
+    newItem.setPayload<KCalCore::Incidence::Ptr>(KCalCore::Incidence::Ptr(incidence->clone()));
+    return newItem;
+}
+
+void InvitationsAgent::itemAdded(const Item &item, const Collection &collection)
+{
+    qDebug() << item.id() << collection;
+    Q_UNUSED(collection);
+
+    if (!m_invitationsCollection->defaultCollection().isValid()) {
+        qDebug() << "No default collection found";
+        return;
+    }
+
+    if (collection.isVirtual()) {
+        return;
+    }
+
+    if (!item.hasPayload<KMime::Message::Ptr>()) {
+        qDebug() << "Item has no payload";
+        return;
+    }
+
+    KMime::Message::Ptr message = item.payload<KMime::Message::Ptr>();
+
+    //TODO check if we are the sender and need to ignore the message...
+    //const QString sender = message->sender()->asUnicodeString();
+    //if ( identityManager()->thatIsMe( sender ) ) return;
+
+    KCalCore::MemoryCalendar::Ptr calendar(new KCalCore::MemoryCalendar(KSystemTimeZones::local()));
+    if (message->contentType()->isMultipart()) {
+        qDebug() << "message is multipart:" << message->attachments().size();
+
+        InvitationsAgentItem *it = Q_NULLPTR;
+        foreach (KMime::Content *content, message->contents()) {
+
+            KMime::Headers::ContentType *ct = content->contentType();
+            Q_ASSERT(ct);
+            qDebug() << "Mimetype of the body part is " << ct->mimeType();
+            if (ct->mimeType() != "text/calendar") {
+                continue;
+            }
+
+            Item newItem = handleContent(QLatin1String(content->body()), calendar, item);
+            if (!newItem.hasPayload()) {
+                qDebug() << "new item has no payload";
+                continue;
+            }
+
+            if (!it) {
+                it = new InvitationsAgentItem(this, item);
+            }
+
+            it->add(newItem);
+        }
+    } else {
+        qDebug() << "message is not multipart";
+
+        KMime::Headers::ContentType *ct = message->contentType();
+        Q_ASSERT(ct);
+        qDebug() << "Mimetype of the body is " << ct->mimeType();
+        if (ct->mimeType() != "text/calendar") {
+            return;
+        }
+
+        qDebug() << "Message has an invitation in the body, processing";
+
+        Item newItem = handleContent(QLatin1String(message->body()), calendar, item);
+        if (!newItem.hasPayload()) {
+            qDebug() << "new item has no payload";
+            return;
+        }
+
+        InvitationsAgentItem *it = new InvitationsAgentItem(this, item);
+        it->add(newItem);
+    }
+}
+
+AKONADI_AGENT_MAIN(InvitationsAgent)
+
diff --git a/agents/invitations/invitationsagent.desktop b/agents/invitations/invitationsagent.desktop
new file mode 100644 (file)
index 0000000..e5000d3
--- /dev/null
@@ -0,0 +1,97 @@
+[Desktop Entry]
+Name=Invitations Dispatcher Agent
+Name[bs]=Agent za raspodjelu poziva
+Name[ca]=Agent distribuïdor d'invitacions
+Name[ca@valencia]=Agent distribuïdor d'invitacions
+Name[cs]=Agent odesílatele pozvánek
+Name[da]=Invitationsafsendingsagent
+Name[de]=Agent zur Einladungs-Auslieferung
+Name[el]=Πράκτορας αποστολής προσκλήσεων
+Name[en_GB]=Invitations Dispatcher Agent
+Name[es]=Agente despachador de invitaciones
+Name[et]=Kutsete edastamise agent
+Name[fi]=Kutsunlähetysagentti
+Name[fr]=Agent de diffusions d'invitations
+Name[gl]=Axente de Despacho de Convites
+Name[hu]=Meghívófeladó ügynök
+Name[ia]=Agente Distributor de Invitationes
+Name[it]=Agente per la consegna degli inviti
+Name[ja]=正体ディスパッチャーエージェント
+Name[kk]=Шақыру реттеуш агенті
+Name[km]=ការ​អញ្ជើញ​​ភ្នាក់ងារ​​កម្មវិធី​បញ្ជូន
+Name[ko]=초대장 가져오기 에이전트
+Name[lt]=Laiškų gijų išdėstymo agentas
+Name[lv]=Ielūgumu nosūtīšanas aģents
+Name[nb]=Agent for invitasjonssendinger
+Name[nds]=Inladenverdeel-Hölper
+Name[nl]=Uitnodigingen-verspreidingsagent
+Name[pl]=Agent wysyłania zaproszeń
+Name[pt]=Agente de Despacho de Convites
+Name[pt_BR]=Agente de encaminhamento de convites
+Name[ro]=Agent de remitere a invitațiilor
+Name[ru]=Агент диспетчера приглашений
+Name[sk]=Agent spracovania pozvánok
+Name[sl]=Posrednik za razpošiljanje povabil
+Name[sr]=Агент отпремања позивница
+Name[sr@ijekavian]=Агент отпремања позивница
+Name[sr@ijekavianlatin]=Agent otpremanja pozivnica
+Name[sr@latin]=Agent otpremanja pozivnica
+Name[sv]=Modul för inbjudningssändning
+Name[tr]=Davetiye Dağıtıcı Programı
+Name[uk]=Агент розподілу запрошень
+Name[x-test]=xxInvitations Dispatcher Agentxx
+Name[zh_CN]=邀请签发代理
+Name[zh_TW]=邀請配送代理程式
+Comment=Dispatches invitations from your calendar
+Comment[bs]=Raspoređuje pozive iz vašeg kalendara
+Comment[ca]=Distribueix invitacions des del calendari
+Comment[ca@valencia]=Distribueix invitacions des del calendari
+Comment[da]=Udsender invitationer fra din kalender
+Comment[de]=Verschickt Einladungen aus Ihren Kalender
+Comment[el]=Διανέμει προσκλήσεις από το ημερολόγιό σας
+Comment[en_GB]=Dispatches invitations from your calendar
+Comment[es]=Remite invitaciones desde su calendario
+Comment[et]=Kutsete edastamine sinu kalendrist
+Comment[fi]=Lähettää kutsuja kalenteristasi
+Comment[fr]=Diffuse les invitations à partir de votre agenda
+Comment[gl]=Xestiona invitacións do calendario.
+Comment[hu]=Meghívásokat kézbesít a naptárából
+Comment[ia]=Expedi invitationes ex tu calendario
+Comment[it]=Consegna inviti dal tuo calendario
+Comment[kk]=Күнтізбеңіздегі шақыруларды үлестіру
+Comment[ko]=달력에서 초대장을 가져옴
+Comment[lt]=Išsiunčia pakvietimus iš Jūsų kalendoriaus
+Comment[nb]=Sender ut invitasjoner fra din kalender
+Comment[nds]=Verdeelt Inladen ut Dien Kalenner
+Comment[nl]=Brengt uitnodigingen uit uw agenda naar elders
+Comment[pl]=Rozsyła zaproszenia z twojego kalendarza
+Comment[pt]=Trata dos convites do seu calendário
+Comment[pt_BR]=Encaminhamento de convites do seu calendário
+Comment[ro]=Remite invitații din calendar
+Comment[ru]=Рассылает приглашения из календаря
+Comment[sk]=Vybavuje pozvánky z vášho kalendára
+Comment[sl]=Odpošlje povabila z vašega koledarja
+Comment[sr]=Отпрема позивнице из вашег календара
+Comment[sr@ijekavian]=Отпрема позивнице из вашег календара
+Comment[sr@ijekavianlatin]=Otprema pozivnice iz vašeg kalendara
+Comment[sr@latin]=Otprema pozivnice iz vašeg kalendara
+Comment[sv]=Skickar inbjudningar från kalendern
+Comment[tr]=Takviminizdeki davetleri yollar
+Comment[uk]=Роповсюджує запрошення на основі даних календаря
+Comment[x-test]=xxDispatches invitations from your calendarxx
+Comment[zh_CN]=从您的日历发送邀请
+Comment[zh_TW]=從您的行事曆中分配邀請
+Type=AkonadiAgent
+Exec=akonadi_invitations_agent
+Icon=mail-folder-outbox
+
+X-Akonadi-Identifier=akonadi_invitations_agent
+#X-Akonadi-Capabilities=Unique,Autostart,NoConfig
+X-Akonadi-Capabilities=NoConfig
+
+#X-Akonadi-MimeTypes=message/rfc822
+#X-Akonadi-MimeTypes=text/calendar
+X-Akonadi-MimeTypes=application/x-vnd.akonadi.calendar.event
+#X-Akonadi-MimeTypes=application/x-vnd.akonadi.calendar.todo
+#X-Akonadi-MimeTypes=application/x-vnd.akonadi.calendar.journal
+#X-Akonadi-MimeTypes=application/x-vnd.akonadi.calendar.freebusy
diff --git a/agents/invitations/invitationsagent.h b/agents/invitations/invitationsagent.h
new file mode 100644 (file)
index 0000000..4d35c50
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+    Copyright 2009 Sebastian Sauer <sebsauer@kdab.net>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef INVITATIONSAGENT_H
+#define INVITATIONSAGENT_H
+
+#include <KCalCore/MemoryCalendar>
+
+#include <AgentBase>
+#include <Collection>
+#include <Item>
+#include <ItemCreateJob>
+
+#include <QtCore/QObject>
+
+class KJob;
+
+class InvitationsAgent;
+class InvitationsCollection;
+
+class InvitationsAgentItem : public QObject
+{
+    Q_OBJECT
+
+public:
+    InvitationsAgentItem(InvitationsAgent *a, const Akonadi::Item &originalItem);
+    virtual ~InvitationsAgentItem();
+    void add(const Akonadi::Item &newItem);
+
+private Q_SLOTS:
+    void createItemResult(KJob *job);
+    void fetchItemDone(KJob *);
+    void modifyItemDone(KJob *job);
+
+private:
+    InvitationsAgent *m_agent;
+    const Akonadi::Item m_originalItem;
+    QList<Akonadi::ItemCreateJob *> m_jobs;
+};
+
+class InvitationsAgent : public Akonadi::AgentBase, public Akonadi::AgentBase::ObserverV2
+{
+    Q_OBJECT
+
+public:
+    explicit InvitationsAgent(const QString &id);
+    virtual ~InvitationsAgent();
+
+    Akonadi::Collection collection();
+
+public Q_SLOTS:
+    void configure(WId windowId) Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void initStart();
+    void initDone(KJob *job = Q_NULLPTR);
+
+private:
+    Akonadi::Item handleContent(const QString &vcal,
+                                const KCalCore::MemoryCalendar::Ptr &calendar,
+                                const Akonadi::Item &item);
+
+    void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+
+    /*
+    virtual void itemChanged( const Akonadi::Item &item, const QSet<QByteArray> &partIdentifiers );
+    virtual void itemRemoved( const Akonadi::Item &item );
+    virtual void collectionAdded( const Akonadi::Collection &collection, const Akonadi::Collection &parent );
+    virtual void collectionChanged( const Akonadi::Collection &collection );
+    virtual void collectionRemoved( const Akonadi::Collection &collection );
+
+    virtual void itemMoved( const Akonadi::Item &item, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination );
+    virtual void itemLinked( const Akonadi::Item &item, const Akonadi::Collection &collection );
+    virtual void itemUnlinked( const Akonadi::Item &item, const Akonadi::Collection &collection );
+    virtual void collectionMoved( const Akonadi::Collection &collection, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination );
+    virtual void collectionChanged( const Akonadi::Collection &collection, const QSet<QByteArray> &changedAttributes );
+    */
+
+private:
+    QString m_resourceId;
+    InvitationsCollection *m_invitationsCollection;
+    Akonadi::Collection m_collection;
+};
+
+#endif // MAILDISPATCHERAGENT_H
diff --git a/agents/maildispatcher/CMakeLists.txt b/agents/maildispatcher/CMakeLists.txt
new file mode 100644 (file)
index 0000000..a2213c8
--- /dev/null
@@ -0,0 +1,58 @@
+
+if (BUILD_TESTING)
+   add_subdirectory( autotests )
+endif()
+
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_maildispatcher_agent\")
+
+set( maildispatcheragent_SRCS
+  maildispatcheragent.cpp
+  outboxqueue.cpp
+  sendjob.cpp
+  sentactionhandler.cpp
+  storeresultjob.cpp
+)
+
+ecm_qt_declare_logging_category(maildispatcheragent_SRCS HEADER maildispatcher_debug.h IDENTIFIER MAILDISPATCHER_LOG CATEGORY_NAME log_maildispatcher)
+
+ki18n_wrap_ui(maildispatcheragent_SRCS settings.ui)
+kconfig_add_kcfg_files(maildispatcheragent_SRCS settings.kcfgc)
+kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/maildispatcheragent.kcfg org.kde.Akonadi.MailDispatcher.Settings)
+qt5_add_dbus_adaptor(maildispatcheragent_SRCS
+  ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.MailDispatcher.Settings.xml settings.h Settings
+)
+qt5_add_dbus_adaptor( maildispatcheragent_SRCS
+  org.freedesktop.Akonadi.MailDispatcherAgent.xml maildispatcheragent.h MailDispatcherAgent
+)
+
+if (RUNTIME_PLUGINS_STATIC)
+  add_definitions(-DMAIL_SERIALIZER_PLUGIN_STATIC)
+endif ()
+
+add_executable(akonadi_maildispatcher_agent ${maildispatcheragent_SRCS})
+
+if (RUNTIME_PLUGINS_STATIC)
+  target_link_libraries(akonadi_maildispatcher_agent akonadi_serializer_mail)
+endif ()
+
+if( APPLE )
+  set_target_properties(akonadi_maildispatcher_agent PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../Info.plist.template)
+  set_target_properties(akonadi_maildispatcher_agent PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.maildispatcher")
+  set_target_properties(akonadi_maildispatcher_agent PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi Maildispatcher")
+endif ()
+
+target_link_libraries(akonadi_maildispatcher_agent 
+    KF5::AkonadiCore
+    KF5::AkonadiMime
+    KF5::Mime
+    KF5::MailTransport
+    KF5::AkonadiAgentBase
+    KF5::NotifyConfig
+    KF5::DBusAddons
+    KF5::I18n
+    KF5::Notifications
+)
+
+install( TARGETS akonadi_maildispatcher_agent ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} )
+install( FILES maildispatcheragent.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents" )
+install( FILES akonadi_maildispatcher_agent.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFY5RCDIR} )
diff --git a/agents/maildispatcher/Messages.sh b/agents/maildispatcher/Messages.sh
new file mode 100755 (executable)
index 0000000..8720606
--- /dev/null
@@ -0,0 +1,4 @@
+#! /bin/sh
+$EXTRACTRC *.ui *.kcfg >> rc.cpp
+$XGETTEXT *.cpp -o $podir/akonadi_maildispatcher_agent.pot
+rm -f rc.cpp
diff --git a/agents/maildispatcher/TODO b/agents/maildispatcher/TODO
new file mode 100644 (file)
index 0000000..d45568e
--- /dev/null
@@ -0,0 +1,16 @@
+* Once MoveJobs work, enable and test moving to sent-mail.  Here is why it
+  doesn't work currently: Akonadi::Monitor sends itemMoved(...), but it is
+  not handled in AgentBase::Observer, and so it never gets "processed", and
+  is stuck in the Monitor/ChangeRecorder forever.  (I.e. the resource will
+  not get any new notifications.) This has to be fixed in Akonadi, but that
+  means adding ObserverV2 or something.
+* Figure out which / whether error strings should be i18n'd
+* Should probably use progressMessage instead of statusMessage, but it seems
+  to be unimplemented in AgentInstance, and only a stub in AgentBase.
+* Do something about timeouts.
+* Test aborting and progress reporting for resource-based transports.
+
+Bugs:
+* Incorrect size reporting in itemChanged() from Monitor.  Leads to displaying
+  >100% progress.
+  (found with: offline, queue some, online, abort, clearerror)
diff --git a/agents/maildispatcher/akonadi_maildispatcher_agent.notifyrc b/agents/maildispatcher/akonadi_maildispatcher_agent.notifyrc
new file mode 100644 (file)
index 0000000..0ca9716
--- /dev/null
@@ -0,0 +1,197 @@
+[Global]
+IconName=mail-folder-outbox
+Comment=KDE e-mail client
+Comment[ast]=Veceru de corréu KDE
+Comment[bg]=Пощенски клиент за KDE
+Comment[bs]=KDE klijent elektronske pošte
+Comment[ca]=Client de correu electrònic pel KDE
+Comment[ca@valencia]=Client de correu electrònic pel KDE
+Comment[cs]=Klient KDE pro čtení elektronické pošty
+Comment[da]=KDE e-mail-klient
+Comment[de]=E-Mail-Programm für KDE
+Comment[el]=Πελάτης ηλ.ταχυδρομείου KDE
+Comment[en_GB]=KDE e-mail client
+Comment[es]=Cliente de correo de KDE
+Comment[et]=KDE e-posti klient
+Comment[fi]=KDE:n sähköpostiohjelma
+Comment[fr]=Logiciel KDE de courrier électronique
+Comment[ga]=Cliant Ríomhphoist KDE
+Comment[gl]=Cliente de correo do KDE
+Comment[hu]=KDE e-mail kliens
+Comment[ia]=Programma KDE de e-posta
+Comment[it]=Programma KDE di posta elettronica
+Comment[ja]=KDE メールクライアント
+Comment[kk]=KDE эл.пошта клиенті
+Comment[km]=កម្មវិធី​អ៊ីមែល​របស់ KDE
+Comment[ko]=KDE 이메일 클라이언트
+Comment[lt]=KDE el. pašto klientas
+Comment[lv]=KDE e-pasta klients
+Comment[mr]=केडीई इ-मेल ग्राहक
+Comment[nb]=KDE E-postklient
+Comment[nds]=Nettpostprogramm för KDE
+Comment[nl]=KDE e-mailclient
+Comment[pa]=KDE ਈ-ਮੇਲ ਕਲਾਇਟ
+Comment[pl]=Program pocztowy KDE
+Comment[pt]=Cliente de e-mail do KDE
+Comment[pt_BR]=Cliente de e-mail do KDE
+Comment[ro]=Program de poștă electronică pentru KDE
+Comment[ru]=Почтовый клиент KDE
+Comment[sk]=KDE poštový klient
+Comment[sl]=KDE-jev poštni odjemalec
+Comment[sr]=КДЕ клијент е‑поште
+Comment[sr@ijekavian]=КДЕ клијент е‑поште
+Comment[sr@ijekavianlatin]=KDE klijent e‑pošte
+Comment[sr@latin]=KDE klijent e‑pošte
+Comment[sv]=KDE E-postklient
+Comment[tr]=KDE e-posta istemcisi
+Comment[uk]=Поштовий клієнт KDE
+Comment[x-test]=xxKDE e-mail clientxx
+Comment[zh_CN]=KDE 邮件客户端
+Comment[zh_TW]=KDE 收發信軟體
+Name=KDE Mail
+Name[bg]=KDE поща
+Name[bs]=KDE Mail
+Name[ca]=Correu del KDE
+Name[ca@valencia]=Correu del KDE
+Name[cs]=Pošta v KDE
+Name[da]=KDE Mail
+Name[de]=KDE E-Mail
+Name[el]=Αλληλογραφία KDE
+Name[en_GB]=KDE Mail
+Name[es]=Correo de KDE
+Name[et]=KDE e-post
+Name[fa]=نامه‌ی کی‌دی‌ای
+Name[fi]=KDE Mail
+Name[fr]=Messagerie KDE
+Name[ga]=Ríomhphost KDE
+Name[gl]=Correo do KDE
+Name[hu]=KDE Mail
+Name[ia]=Posta KDE
+Name[it]=KDE Mail
+Name[ja]=KDE Mail
+Name[kk]=KDE поштасы
+Name[km]=សំបុត្រ​របស់ KDE
+Name[ko]=KDE 메일
+Name[lt]=KDE paštas
+Name[lv]=KDE pasts
+Name[mr]=केडीई मेल
+Name[nb]=KDE Mail
+Name[nds]=KDE-Nettpost
+Name[nl]=KDE E-mail
+Name[pa]=KDE ਮੇਲ
+Name[pl]=Poczta KDE
+Name[pt]=Correio do KDE
+Name[pt_BR]=E-mail do KDE
+Name[ro]=Poșta KDE
+Name[ru]=Почта KDE
+Name[sk]=KDE Mail
+Name[sl]=KDE-jeva pošta
+Name[sr]=КДЕ пошта
+Name[sr@ijekavian]=КДЕ пошта
+Name[sr@ijekavianlatin]=KDE pošta
+Name[sr@latin]=KDE pošta
+Name[sv]=KDE:s e-postprogram
+Name[tr]=KDE E-Posta
+Name[uk]=Пошта KDE
+Name[x-test]=xxKDE Mailxx
+Name[zh_CN]=KDE 电子邮件
+Name[zh_TW]=KDE 郵件軟體
+
+[Event/emailsent]
+Name=E-mail successfully sent
+Name[bg]=Е-пощата е изпратена успешно
+Name[bs]=E-mail uspješno poslan
+Name[ca]=Correu electrònic enviat correctament
+Name[ca@valencia]=Correu electrònic enviat correctament
+Name[cs]=E-mail úspěšně odeslán
+Name[da]=E-mail sendt
+Name[de]=E-Mail erfolgreich gesendet
+Name[el]=Η αλληλογραφία στάλθηκε με επιτυχία
+Name[en_GB]=E-mail successfully sent
+Name[es]=Correo enviado satisfactoriamente
+Name[et]=E-kiri saadeti edukalt ära
+Name[fa]=ای‌میل با موفقیت ارسال شد
+Name[fi]=Sähköpostin lähetys onnistui
+Name[fr]=Courrier électronique envoyé avec succès
+Name[gl]=Enviouse a mensaxe sen problemas
+Name[hu]=Az e-mail sikeresen elküldve
+Name[ia]=E-posta inviate successosemente
+Name[it]=Messaggio di posta inviato con successo
+Name[kk]=Эл.пошта сәтті жіберілді
+Name[km]=បាន​ផ្ញើ​អ៊ីមែល​ដោយ​ជោគជ័យ
+Name[ko]=이메일을 성공적으로 전송함
+Name[lt]=El. laiškas sėkmingai išsiųstas
+Name[lv]=E-pasta veiksmīgi nosūtīts
+Name[nb]=E.post vellykket sendt
+Name[nds]=Nettbreef mit Spood loosstüert
+Name[nl]=E-mailbericht met succes verzonden
+Name[pa]=ਈਮੇਲ ਠੀਕ ਤਰ੍ਹਾਂ ਭੇਜੀ ਗਈ
+Name[pl]=Poczta została wysłana pomyślnie
+Name[pt]=O e-mail foi enviado com sucesso
+Name[pt_BR]=E-mail enviado com sucesso
+Name[ro]=Scrisoare expediată cu succes
+Name[ru]=Почта успешно отправлена
+Name[sk]=E-mail úspešne odoslaný
+Name[sl]=E-pošta uspešno poslana
+Name[sr]=Е‑пошта је успешно послата
+Name[sr@ijekavian]=Е‑пошта је успешно послата
+Name[sr@ijekavianlatin]=E‑pošta je uspešno poslata
+Name[sr@latin]=E‑pošta je uspešno poslata
+Name[sv]=E-post skickad med lyckat resultat
+Name[tr]=E-posta başarıyla gönderildi
+Name[ug]=ئېلخەت مۇۋەپپەقىيەتلىك يوللاندى
+Name[uk]=Пошту успішно надіслано
+Name[x-test]=xxE-mail successfully sentxx
+Name[zh_CN]=邮件已成功发送
+Name[zh_TW]=電子郵件已成功傳送
+Action=Popup
+IconName=mail-folder-outbox
+
+[Event/sendingfailed]
+Name=E-mail sending failed
+Name[bg]=Грешка при изпращане на е-пощата
+Name[bs]=Slanje elektronske pošte neuspjelo
+Name[ca]=Ha fallat en enviar el correu electrònic
+Name[ca@valencia]=Ha fallat en enviar el correu electrònic
+Name[cs]=Odesílání e-mailu selhalo
+Name[da]=Afsendelse af e-mail mislykkedes
+Name[de]=Fehler beim Versenden der E-Mail
+Name[el]=Η αποστολή της αλληλογραφίας απέτυχε
+Name[en_GB]=E-mail sending failed
+Name[es]=Fallo en el envío del correo
+Name[et]=E-kirja saatmine nurjus
+Name[fa]=ارسال ای‌میل شکست خورد
+Name[fi]=Sähköpostin lähetys epäonnistui
+Name[fr]=Erreur lors de l'envoi du courrier électronique
+Name[gl]=Produciuse un fallo ao enviar o correo
+Name[hu]=Az e-mail küldése sikertelen
+Name[ia]=Expedition de e-posta falleva
+Name[it]=Invio del messaggio di posta non riuscito
+Name[kk]=Эл.поштаны жолдау жаңылысы
+Name[km]=បាន​បរាជ័យ​ក្នុង​ការ​ផ្ញើ​អ៊ីមែល
+Name[ko]=이메일 전송 실패
+Name[lt]=El. pašto siuntimas nepavyko
+Name[lv]=E-pasta sūtīšana neizdevās
+Name[nb]=E-postsending mislyktes
+Name[nds]=Nettbreef lett sik nich loosstüern
+Name[nl]=Verzenden van e-mail is mislukt
+Name[pa]=ਈਮੇਲ ਭੇਜਣ ਲਈ ਫੇਲ੍ਹ
+Name[pl]=Poczta nie została wysłana pomyślnie
+Name[pt]=O envio do e-mail foi mal-sucedido
+Name[pt_BR]=Falha no envio do e-mail
+Name[ro]=Expedierea scrisorii a eșuat
+Name[ru]=Не удалось отправить почту
+Name[sk]=Poslanie správy zlyhalo
+Name[sl]=Pošiljanje e-pošte ni uspelo
+Name[sr]=Слање е‑поште није успело
+Name[sr@ijekavian]=Слање е‑поште није успело
+Name[sr@ijekavianlatin]=Slanje e‑pošte nije uspelo
+Name[sr@latin]=Slanje e‑pošte nije uspelo
+Name[sv]=Misslyckades skicka brev
+Name[tr]=E-posta gönderimi başarısız oldu
+Name[uk]=Спроба надсилання повідомлення зазнала невдачі
+Name[x-test]=xxE-mail sending failedxx
+Name[zh_CN]=发送邮件失败
+Name[zh_TW]=傳送信件失敗
+Action=Popup
+IconName=mail-mark-junk
diff --git a/agents/maildispatcher/autotests/CMakeLists.txt b/agents/maildispatcher/autotests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..52f03c8
--- /dev/null
@@ -0,0 +1,48 @@
+include(ECMMarkAsTest)
+
+find_package(Qt5Test CONFIG REQUIRED)
+
+set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} )
+
+# Stolen from kdepimlibs/akonadi/tests
+macro(add_akonadi_isolated_test _source)
+  get_filename_component(_targetName ${_source} NAME_WE)
+  set(_srcList ${_source} )
+
+  add_executable(${_targetName} ${_srcList})
+  ecm_mark_as_test(maildispatcher-${_targetName})
+  target_link_libraries(${_targetName}
+    Qt5::Test
+    KF5::AkonadiCore
+    KF5::AkonadiMime
+    KF5::MailTransport
+    KF5::Mime
+    KF5::I18n
+    KF5::ConfigGui
+    Qt5::DBus
+    Qt5::Widgets
+  )
+
+  # based on kde4_add_unit_test
+  if (WIN32)
+    get_target_property( _loc ${_targetName} LOCATION )
+    set(_executable ${_loc}.bat)
+  else ()
+    set(_executable ${EXECUTABLE_OUTPUT_PATH}/${_targetName})
+  endif ()
+  if (UNIX)
+    set(_executable ${_executable}.shell)
+  endif ()
+
+  find_program(_testrunner akonaditest)
+
+  if (KDEPIM_RUN_ISOLATED_TESTS)
+    add_test( maildispatcheragent-${_targetName} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config.xml ${_executable} )
+  endif ()
+endmacro(add_akonadi_isolated_test)
+
+
+
+add_akonadi_isolated_test( aborttest.cpp )
+add_akonadi_isolated_test( dupetest.cpp )
+
diff --git a/agents/maildispatcher/autotests/TODO b/agents/maildispatcher/autotests/TODO
new file mode 100644 (file)
index 0000000..1cdff10
--- /dev/null
@@ -0,0 +1,6 @@
+* test online / offline
+* figure out why ksyscoca is started (it's not the wallet apparently)
+* aborttest: test aborting when there is >1 message queued
+* test the various SendBehaviours and DispatchModes
+* dupetest -> probably faster and more effective if I just use random intervals
+  of time between queuing messages
diff --git a/agents/maildispatcher/autotests/aborttest.cpp b/agents/maildispatcher/autotests/aborttest.cpp
new file mode 100644 (file)
index 0000000..5c667b4
--- /dev/null
@@ -0,0 +1,239 @@
+/*
+    Copyright 2009 Constantin Berzan <exit3219@gmail.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "aborttest.h"
+
+#include <QDBusInterface>
+#include <QDBusReply>
+
+#include <QDebug>
+
+#include <AkonadiCore/AgentInstance>
+#include <AkonadiCore/AgentManager>
+#include <AkonadiCore/collectionstatistics.h>
+#include <AkonadiCore/Control>
+#include <AkonadiCore/ItemDeleteJob>
+#include <AkonadiCore/ItemFetchJob>
+#include <AkonadiCore/ItemFetchScope>
+#include <AkonadiCore/ItemModifyJob>
+#include <AkonadiCore/Monitor>
+#include <AkonadiCore/qtest_akonadi.h>
+#include <AkonadiCore/collectionpathresolver.h>
+#include <Akonadi/KMime/MessageFlags>
+#include <Akonadi/KMime/SpecialMailCollections>
+#include <Akonadi/KMime/SpecialMailCollectionsRequestJob>
+
+#include <mailtransport/dispatcherinterface.h>
+#include <mailtransport/errorattribute.h>
+#include <mailtransport/messagequeuejob.h>
+#include <mailtransport/transport.h>
+#include <mailtransport/transportattribute.h>
+#include <mailtransport/transportmanager.h>
+
+#include <kmime/kmime_message.h>
+
+#include <QSignalSpy>
+
+#define SPAM_ADDRESS "idanoka@gmail.com"
+// NOTE: This test relies on a large SMTP message taking long enough to deliver,
+// for it to call abort.  So we need a valid receiver and a not-too-fast connection.
+#define MESSAGE_MB 1
+
+using namespace Akonadi;
+using namespace KMime;
+using namespace MailTransport;
+
+void AbortTest::initTestCase()
+{
+    QVERIFY(Control::start());
+    QTest::qWait(1000);
+
+    qRegisterMetaType<Akonadi::Item>();
+    qRegisterMetaType<Akonadi::Collection>();
+
+    // Get the outbox and clear it.
+    SpecialMailCollectionsRequestJob *rjob = new SpecialMailCollectionsRequestJob(this);
+    rjob->requestDefaultCollection(SpecialMailCollections::Outbox);
+    QSignalSpy spy(rjob, SIGNAL(result(KJob*)));
+    spy.wait(0);
+    outbox = SpecialMailCollections::self()->defaultCollection(SpecialMailCollections::Outbox);
+    QVERIFY(outbox.isValid());
+    ItemDeleteJob *djob = new ItemDeleteJob(outbox);
+    djob->exec(); // may give error if outbox empty
+
+    // Verify transports.
+    akoTid = TransportManager::self()->defaultTransportId();
+    Transport *t = TransportManager::self()->transportById(akoTid);
+    QVERIFY(t);
+    QCOMPARE(t->type(), int(Transport::EnumType::Akonadi));
+    QList<int> tids = TransportManager::self()->transportIds();
+    tids.removeAll(akoTid);
+    QCOMPARE(tids.count(), 1);
+    smtpTid = tids.first();
+    t = TransportManager::self()->transportById(smtpTid);
+    QVERIFY(t);
+    QCOMPARE(t->type(), int(Transport::EnumType::SMTP));
+
+    // Set sink collection.
+    t = TransportManager::self()->transportById(akoTid);
+    const QString rid = t->host();
+    const AgentInstance agent = AgentManager::self()->instance(rid);
+    QVERIFY(agent.isValid());
+    CollectionPathResolver *resolver = new CollectionPathResolver(QStringLiteral("sink"), this);
+    QVERIFY(resolver->exec());
+    sink = Collection(resolver->collection());
+    QVERIFY(sink.isValid());
+    QDBusInterface conf(QLatin1String("org.freedesktop.Akonadi.Resource.") + rid,
+                        QStringLiteral("/Settings"), QStringLiteral("org.kde.Akonadi.MailTransportDummy.Settings"));
+    QVERIFY(conf.isValid());
+    QDBusReply<void> reply = conf.call(QStringLiteral("setSink"), sink.id());
+    QVERIFY(reply.isValid());
+    agent.reconfigure();
+
+    // Watch sink collection.
+    monitor = new Monitor(this);
+    monitor->setCollectionMonitored(sink);
+}
+
+void AbortTest::testAbort()
+{
+    // Get the MDA interface.
+    DispatcherInterface iface;
+    QVERIFY(iface.dispatcherInstance().isValid());
+    QVERIFY(iface.dispatcherInstance().isOnline());
+
+    // Create a large message.
+    qDebug() << "Building message.";
+    Message::Ptr msg = Message::Ptr(new Message);
+    QByteArray line(70, 'a');
+    line.append("\n");
+    QByteArray content("\n");
+    for (int i = 0; i < MESSAGE_MB * 1024 * 1024 / line.length() + 10; i++) {
+        content.append(line);
+    }
+    QVERIFY(content.length() > MESSAGE_MB * 1024 * 1024);   // >10MiB
+    msg->setContent(content);
+
+    // Queue the message.
+    qDebug() << "Queuing message.";
+    MessageQueueJob *qjob = new MessageQueueJob(this);
+    qjob->setMessage(msg);
+    qjob->transportAttribute().setTransportId(smtpTid);
+    // default dispatch mode
+    // default sent-mail collection
+    qjob->addressAttribute().setFrom(QStringLiteral("naiba"));
+    qjob->addressAttribute().setTo(QStringList() << QStringLiteral(SPAM_ADDRESS));
+    QCOMPARE(iface.dispatcherInstance().status(), AgentInstance::Idle);
+    AKVERIFYEXEC(qjob);
+
+    // Wait for the MDA to begin dispatching.
+    for (int ds = 0; iface.dispatcherInstance().status() == AgentInstance::Idle; ds++) {
+        QTest::qWait(100);
+        if (ds % 10 == 0) {
+            qDebug() << "Waiting for the MDA to begin dispatching." << ds / 10 << "seconds elapsed.";
+        }
+
+        QVERIFY2(ds <= 100, "Timeout");
+    }
+    QTest::qWait(100);
+
+    // Tell the MDA to abort.
+    QCOMPARE(iface.dispatcherInstance().status(), AgentInstance::Running);
+    iface.dispatcherInstance().abortCurrentTask();
+    for (int ds = 0; iface.dispatcherInstance().status() != AgentInstance::Idle; ds++) {
+        QTest::qWait(100);
+        if (ds % 10 == 0) {
+            qDebug() << "Waiting for the MDA to become idle after aborting." << ds / 10 << "seconds elapsed.";
+        }
+
+        QVERIFY2(ds <= 100, "Timeout");
+    }
+    QCOMPARE(iface.dispatcherInstance().status(), AgentInstance::Idle);
+
+    // Verify that item has an ErrorAttribute.
+    ItemFetchJob *fjob = new ItemFetchJob(outbox);
+    fjob->fetchScope().fetchAllAttributes();
+    AKVERIFYEXEC(fjob);
+    QCOMPARE(fjob->items().count(), 1);
+    Item item = fjob->items().at(0);
+    QVERIFY(item.hasAttribute<ErrorAttribute>());
+    ErrorAttribute *eA = item.attribute<ErrorAttribute>();
+    qDebug() << "Stored error:" << eA->message();
+
+    // "Fix" the item and send again, this time with the default (Akonadi) transport.
+    item.removeAttribute<ErrorAttribute>();
+    item.clearFlag(Akonadi::MessageFlags::HasError);
+    item.setFlag(Akonadi::MessageFlags::Queued);
+    TransportAttribute *newTA = new TransportAttribute(akoTid);
+    item.addAttribute(newTA);
+    ItemModifyJob *cjob = new ItemModifyJob(item);
+    QSignalSpy *addSpy = new QSignalSpy(monitor, SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection)));
+    AKVERIFYEXEC(cjob);
+
+    // Verify that the item got sent.
+    for (int ds = 0; addSpy->isEmpty(); ds++) {
+        QTest::qWait(100);
+        if (ds % 10 == 0) {
+            qDebug() << "Waiting for an item to be sent." << ds / 10 << "seconds elapsed.";
+        }
+
+        QVERIFY2(ds <= 100, "Timeout");
+    }
+    QCOMPARE(addSpy->count(), 1);
+    QCOMPARE(iface.dispatcherInstance().status(), AgentInstance::Idle);
+}
+
+void AbortTest::testAbortWhileIdle()
+{
+    // Get the MDA interface.
+    DispatcherInterface iface;
+    QVERIFY(iface.dispatcherInstance().isValid());
+    QVERIFY(iface.dispatcherInstance().isOnline());
+
+    // Abort thin air.
+    QCOMPARE(iface.dispatcherInstance().status(), AgentInstance::Idle);
+    iface.dispatcherInstance().abortCurrentTask();
+    QCOMPARE(iface.dispatcherInstance().status(), AgentInstance::Idle);
+
+    // Queue a message (to check that subsequent messages are being sent).
+    QVERIFY(monitor);
+    QSignalSpy *addSpy = new QSignalSpy(monitor, SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection)));
+    Message::Ptr msg = Message::Ptr(new Message);
+    msg->setContent("\ntestAbortWhileIdle");
+    MessageQueueJob *qjob = new MessageQueueJob(this);
+    qjob->setMessage(msg);
+    qjob->transportAttribute().setTransportId(akoTid);
+    // default dispatch mode
+    // default sent-mail collection
+    qjob->addressAttribute().setFrom(QStringLiteral("naiba"));
+    qjob->addressAttribute().setTo(QStringList() << QStringLiteral("dracu"));
+    QCOMPARE(iface.dispatcherInstance().status(), AgentInstance::Idle);
+    AKVERIFYEXEC(qjob);
+
+    // Verify that the item got sent.
+    for (int s = 0; addSpy->isEmpty(); s++) {
+        QTest::qWait(1000);
+        QVERIFY2(s <= 10, "Timeout");
+    }
+    QCOMPARE(addSpy->count(), 1);
+    QCOMPARE(iface.dispatcherInstance().status(), AgentInstance::Idle);
+}
+
+QTEST_AKONADIMAIN(AbortTest)
+
diff --git a/agents/maildispatcher/autotests/aborttest.h b/agents/maildispatcher/autotests/aborttest.h
new file mode 100644 (file)
index 0000000..3235a96
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+    Copyright 2009 Constantin Berzan <exit3219@gmail.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef ABORTTEST_H
+#define ABORTTEST_H
+
+#include <QtCore/QObject>
+
+#include <AkonadiCore/collection.h>
+
+namespace Akonadi
+{
+class Monitor;
+}
+
+/**
+  This attempts to send a large message, then aborts it, then tries to send
+  it again and verify that it succeeds.
+ */
+class AbortTest : public QObject
+{
+    Q_OBJECT
+
+private Q_SLOTS:
+    void initTestCase();
+    void testAbort();
+    void testAbortWhileIdle();
+
+private:
+    int akoTid;
+    int smtpTid;
+    Akonadi::Collection outbox;
+    Akonadi::Collection sink;
+    Akonadi::Monitor *monitor;
+
+};
+
+#endif
diff --git a/agents/maildispatcher/autotests/dupetest.cpp b/agents/maildispatcher/autotests/dupetest.cpp
new file mode 100644 (file)
index 0000000..a1f4b67
--- /dev/null
@@ -0,0 +1,220 @@
+/*
+    Copyright 2009 Constantin Berzan <exit3219@gmail.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "dupetest.h"
+
+#include <QDBusInterface>
+#include <QDBusReply>
+
+#include <QDebug>
+
+#include <AkonadiCore/Control>
+#include <AkonadiCore/AgentInstance>
+#include <AkonadiCore/AgentManager>
+#include <AkonadiCore/CollectionFetchJob>
+#include <AkonadiCore/ItemDeleteJob>
+#include <AkonadiCore/ItemFetchJob>
+#include <AkonadiCore/ItemFetchScope>
+#include <AkonadiCore/qtest_akonadi.h>
+#include <AkonadiCore/collectionpathresolver.h>
+
+#include <mailtransport/messagequeuejob.h>
+#include <mailtransport/transport.h>
+#include <mailtransport/transportmanager.h>
+
+#include <kmime/kmime_message.h>
+
+static const int TIMEOUT_SECONDS = 60;
+static const int MAXCOUNT = 99; // must be 2-digit!
+
+using namespace Akonadi;
+using namespace KMime;
+using namespace MailTransport;
+
+void DupeTest::initTestCase()
+{
+    QVERIFY(Control::start());
+    QTest::qWait(1000);   // give the MDA time to start
+
+    qRegisterMetaType<Akonadi::Item>();
+    qRegisterMetaType<Akonadi::Collection>();
+
+    // we need a default Akonadi transport
+    int tid = TransportManager::self()->defaultTransportId();
+    Transport *t = TransportManager::self()->transportById(tid);
+    QVERIFY(t);
+    QCOMPARE(t->type(), int(Transport::EnumType::Akonadi));
+
+    // set the sink collection
+    const QString rid = t->host();
+    const AgentInstance agent = AgentManager::self()->instance(rid);
+    QVERIFY(agent.isValid());
+    CollectionPathResolver *resolver = new CollectionPathResolver(QStringLiteral("sink"), this);
+    QVERIFY(resolver->exec());
+    sink = Collection(resolver->collection());
+    QVERIFY(sink.isValid());
+    QDBusInterface conf(QLatin1String("org.freedesktop.Akonadi.Resource.") + rid,
+                        QStringLiteral("/Settings"), QStringLiteral("org.kde.Akonadi.MailTransportDummy.Settings"));
+    QVERIFY(conf.isValid());
+    QDBusReply<void> reply = conf.call(QStringLiteral("setSink"), sink.id());
+    QVERIFY(reply.isValid());
+    agent.reconfigure();
+
+    // set up monitor
+    monitor = new Monitor(this);
+    monitor->setCollectionMonitored(sink);
+    monitor->itemFetchScope().fetchFullPayload();
+}
+
+void DupeTest::testDupes_data()
+{
+    QTest::addColumn<QString>("message");   // the prefix of the message to send (-msg## is appended)
+    QTest::addColumn<int>("count");   // how many copies to send
+    QTest::addColumn<int>("delay");   // number of ms to wait before sending next copy
+
+    QTest::newRow("1-nodelay") << "\n1-nodelay" << 1 << 0;
+    QTest::newRow("2-nodelay") << "\n2-nodelay" << 2 << 0;
+    QTest::newRow("5-nodelay") << "\n5-nodelay" << 5 << 0;
+    QTest::newRow("10-nodelay") << "\n10-nodelay" << 10 << 0;
+    QTest::newRow("20-nodelay") << "\n20-nodelay" << 20 << 0;
+    QTest::newRow("50-nodelay") << "\n50-nodelay" << 50 << 0;
+    QTest::newRow("99-nodelay") << "\n99-nodelay" << 99 << 0;
+    QTest::newRow("2-veryshortdelay") << "\n2-veryshortdelay" << 2 << 20;
+    QTest::newRow("5-veryshortdelay") << "\n5-veryshortdelay" << 5 << 20;
+    QTest::newRow("10-veryshortdelay") << "\n10-veryshortdelay" << 10 << 20;
+    QTest::newRow("20-veryshortdelay") << "\n20-veryshortdelay" << 20 << 20;
+    QTest::newRow("50-veryshortdelay") << "\n50-veryshortdelay" << 50 << 20;
+    QTest::newRow("99-veryshortdelay") << "\n99-veryshortdelay" << 99 << 20;
+    QTest::newRow("2-shortdelay") << "\n2-shortdelay" << 2 << 100;
+    QTest::newRow("5-shortdelay") << "\n5-shortdelay" << 5 << 100;
+    QTest::newRow("10-shortdelay") << "\n10-shortdelay" << 10 << 100;
+    QTest::newRow("20-shortdelay") << "\n20-shortdelay" << 20 << 100;
+    QTest::newRow("50-shortdelay") << "\n50-shortdelay" << 50 << 100;
+    QTest::newRow("99-shortdelay") << "\n99-shortdelay" << 99 << 99;
+    QTest::newRow("2-longdelay") << "\n2-longdelay" << 2 << 1000;
+    QTest::newRow("5-longdelay") << "\n5-longdelay" << 5 << 1000;
+    QTest::newRow("5-verylongdelay") << "\n5-verylongdelay" << 5 << 4000;
+    Q_ASSERT(99 <= MAXCOUNT);
+    Q_ASSERT(MAXCOUNT < 100);   // 2-digit
+
+    // TODO I'm not sure more items means a better test
+    // TODO test large items
+    // TODO test modifying items while they are being sent...
+}
+
+void DupeTest::testDupes()
+{
+    QFETCH(QString, message);
+    QFETCH(int, count);
+    QFETCH(int, delay);
+
+    // clean sink
+    ItemFetchJob *fjob = new ItemFetchJob(sink, this);
+    AKVERIFYEXEC(fjob);
+    if (fjob->items().count() > 0) {
+        // this test is needed because ItemDeleteJob gives error if no items are found
+        ItemDeleteJob *djob = new ItemDeleteJob(sink, this);
+        AKVERIFYEXEC(djob);
+    }
+    fjob = new ItemFetchJob(sink, this);
+    AKVERIFYEXEC(fjob);
+    QCOMPARE(fjob->items().count(), 0);
+
+    // queue messages
+    Q_ASSERT(monitor);
+    QSignalSpy *addSpy = new QSignalSpy(monitor, SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection)));
+    qDebug() << "Queuing" << count << "messages...";
+    for (int i = 0; i < count; i++) {
+        //qDebug() << "Queuing message" << i + 1 << "of" << count;
+
+        Message::Ptr msg = Message::Ptr(new Message);
+        msg->setContent(QStringLiteral("%1-msg%2\n").arg(message).arg(i + 1, 2, 10, QLatin1Char('0')).toLatin1());
+
+        MessageQueueJob *job = new MessageQueueJob(this);
+        job->setMessage(msg);
+        job->transportAttribute().setTransportId(TransportManager::self()->defaultTransportId());
+        // default dispatch mode
+        // default sent-mail collection
+        job->addressAttribute().setFrom(QStringLiteral("naiba"));
+        job->addressAttribute().setTo(QStringList() << QStringLiteral("dracu"));
+        //AKVERIFYEXEC( job );
+        job->start();
+        QTest::qWait(delay);
+    }
+    qDebug() << "Queued" << count << "messages.";
+
+    // wait for the MDA to send them
+    int seconds = 0;
+    while (true) {
+        seconds++;
+        QTest::qWait(1000);
+        qDebug() << seconds << "seconds elapsed." << addSpy->count() << "messages got to sink.";
+        if (addSpy->count() >= count) {
+            break;
+        }
+
+#if 0
+        if (seconds >= TIMEOUT_SECONDS) {
+            qDebug() << "Timeout, gdb master!";
+            QTest::qWait(1000 * 1000);
+        }
+#endif
+        QVERIFY2(seconds < TIMEOUT_SECONDS, "Timeout");
+    }
+
+    // TODO I should verify that the MDA has actually finished its work and has an empty queue
+    QTest::qWait(2000);
+
+    // verify what has been sent
+    fjob = new ItemFetchJob(sink, this);
+    fjob->fetchScope().fetchFullPayload();
+    AKVERIFYEXEC(fjob);
+    const Item::List items = fjob->items();
+    int found[ MAXCOUNT ];
+    for (int i = 0; i < count; i++) {
+        found[i] = 0;
+    }
+    for (int i = 0; i < items.count(); i++) {
+        QVERIFY(items[i].hasPayload<Message::Ptr>());
+        Message::Ptr msg = items[i].payload<Message::Ptr>();
+        const QByteArray content = msg->encodedContent();
+        //qDebug() << "i" << i << "content" << content;
+        int loc = content.indexOf("-msg");
+        QVERIFY(loc >= 0);
+        bool ok;
+        int who = content.mid(loc + 4, 2).toInt(&ok);
+        QVERIFY(ok);
+        //qDebug() << "identified msg" << who;
+        QVERIFY(who > 0 && who <= count);
+        found[ who - 1 ]++;
+    }
+    for (int i = 0; i < count; i++) {
+        if (found[i] > 1) {
+            qDebug() << "found duplicate message" << i + 1 << "(" << found[i] << "times )";
+        } else if (found[i] < 1) {
+            qDebug() << "didn't find message" << i + 1;
+        }
+        QCOMPARE(found[i], 1);
+    }
+    QCOMPARE(addSpy->count(), count);
+    QCOMPARE(items.count(), count);
+}
+
+QTEST_AKONADIMAIN(DupeTest)
+
diff --git a/agents/maildispatcher/autotests/dupetest.h b/agents/maildispatcher/autotests/dupetest.h
new file mode 100644 (file)
index 0000000..b78516d
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+    Copyright 2009 Constantin Berzan <exit3219@gmail.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef DUPETEST_H
+#define DUPETEST_H
+
+#include <QtCore/QObject>
+
+#include <AkonadiCore/Collection>
+#include <AkonadiCore/Monitor>
+
+/**
+  This queues a bunch of messages very quickly one after the other, lets the
+  MDA send them via the dummy mailtransport resource, and then verify that the
+  correct number of messages has been sent.
+ */
+class DupeTest : public QObject
+{
+    Q_OBJECT
+
+private Q_SLOTS:
+    void initTestCase();
+    void testDupes_data();
+    void testDupes();
+
+private:
+    Akonadi::Collection sink;
+    Akonadi::Monitor *monitor;
+
+};
+
+#endif
diff --git a/agents/maildispatcher/autotests/unittestenv/config.xml b/agents/maildispatcher/autotests/unittestenv/config.xml
new file mode 100644 (file)
index 0000000..248ebb5
--- /dev/null
@@ -0,0 +1,7 @@
+<config>
+  <kdehome>kdehome</kdehome>
+  <confighome>xdgconfig</confighome>
+  <datahome>xdglocal</datahome>
+  <agent synchronize="true">akonadi_knut_resource</agent>
+  <agent synchronize="true">akonadi_mailtransport_dummy_resource</agent>
+</config>
diff --git a/agents/maildispatcher/autotests/unittestenv/kdehome/share/config/akonadi-firstrunrc b/agents/maildispatcher/autotests/unittestenv/kdehome/share/config/akonadi-firstrunrc
new file mode 100644 (file)
index 0000000..1cac492
--- /dev/null
@@ -0,0 +1,3 @@
+[ProcessedDefaults]
+defaultaddressbook=done
+defaultcalendar=done
diff --git a/agents/maildispatcher/autotests/unittestenv/kdehome/share/config/akonadi_knut_resource_0rc b/agents/maildispatcher/autotests/unittestenv/kdehome/share/config/akonadi_knut_resource_0rc
new file mode 100644 (file)
index 0000000..fb457b1
--- /dev/null
@@ -0,0 +1,4 @@
+[General]
+DataFile[$e]=$KDEHOME/testdata.xml
+FileWatchingEnabled=false
+
diff --git a/agents/maildispatcher/autotests/unittestenv/kdehome/share/config/akonadi_mailtransport_dummy_resource_0rc b/agents/maildispatcher/autotests/unittestenv/kdehome/share/config/akonadi_mailtransport_dummy_resource_0rc
new file mode 100644 (file)
index 0000000..6d16049
--- /dev/null
@@ -0,0 +1,2 @@
+[General]
+Sink=123
diff --git a/agents/maildispatcher/autotests/unittestenv/kdehome/share/config/kdebugrc b/agents/maildispatcher/autotests/unittestenv/kdehome/share/config/kdebugrc
new file mode 100644 (file)
index 0000000..32317f7
--- /dev/null
@@ -0,0 +1,110 @@
+[0]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[5250]
+InfoOutput=2
+
+[5251]
+InfoOutput=2
+
+[5252]
+InfoOutput=2
+
+[5253]
+InfoOutput=2
+
+[5254]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[5255]
+InfoOutput=2
+
+[5256]
+InfoOutput=2
+
+[5257]
+InfoOutput=2
+
+[5258]
+InfoOutput=2
+
+[5259]
+InfoOutput=2
+
+[5260]
+InfoOutput=2
+
+[5261]
+InfoOutput=2
+
+[5262]
+InfoOutput=2
+
+[5263]
+InfoOutput=2
+
+[5264]
+InfoOutput=2
+
+[5265]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[5266]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[5295]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[5324]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[7129]
+InfoOutput=2
diff --git a/agents/maildispatcher/autotests/unittestenv/kdehome/share/config/kdedrc b/agents/maildispatcher/autotests/unittestenv/kdehome/share/config/kdedrc
new file mode 100644 (file)
index 0000000..41d1781
--- /dev/null
@@ -0,0 +1,3 @@
+[General]
+CheckSycoca=false
+CheckFileStamps=false
diff --git a/agents/maildispatcher/autotests/unittestenv/kdehome/share/config/kwalletrc b/agents/maildispatcher/autotests/unittestenv/kdehome/share/config/kwalletrc
new file mode 100644 (file)
index 0000000..8ba29ca
--- /dev/null
@@ -0,0 +1,2 @@
+[Wallet]
+Enabled=false
diff --git a/agents/maildispatcher/autotests/unittestenv/kdehome/share/config/mailtransports b/agents/maildispatcher/autotests/unittestenv/kdehome/share/config/mailtransports
new file mode 100644 (file)
index 0000000..13ac7b0
--- /dev/null
@@ -0,0 +1,23 @@
+[$Version]
+update_info=mailtransports.upd:initial-kmail-migration,mailtransports.upd:initial-knode-migration
+
+[General]
+default-transport=666
+
+[Transport 666]
+host=akonadi_mailtransport_dummy_resource_0
+id=666
+name=Dummy Akonadi Transport
+type=Akonadi
+
+[Transport 549190884]
+auth=true
+encryption=SSL
+host=smtp.gmail.com
+id=549190884
+name=idanoka2-stored
+password=ᄒᄡᄚᄆᄒᄏᄊᆱᄎᆲᆱ
+port=465
+storepass=true
+user=idanoka2@gmail.com
+
diff --git a/agents/maildispatcher/autotests/unittestenv/kdehome/share/config/qttestrc b/agents/maildispatcher/autotests/unittestenv/kdehome/share/config/qttestrc
new file mode 100644 (file)
index 0000000..2e2f28e
--- /dev/null
@@ -0,0 +1,2 @@
+[Notification Messages]
+WalletMigrate=false
diff --git a/agents/maildispatcher/autotests/unittestenv/kdehome/testdata.xml b/agents/maildispatcher/autotests/unittestenv/kdehome/testdata.xml
new file mode 100644 (file)
index 0000000..8cf871b
--- /dev/null
@@ -0,0 +1,4 @@
+<knut>
+ <collection rid="123" name="sink" content="inode/directory,message/rfc822">
+ </collection>
+</knut>
diff --git a/agents/maildispatcher/autotests/unittestenv/xdgconfig/akonadi/akonadiserverrc b/agents/maildispatcher/autotests/unittestenv/xdgconfig/akonadi/akonadiserverrc
new file mode 100644 (file)
index 0000000..7f738ce
--- /dev/null
@@ -0,0 +1,4 @@
+[%General]
+
+[Search]
+Manager=Dummy
diff --git a/agents/maildispatcher/autotests/unittestenv/xdglocal/.keep b/agents/maildispatcher/autotests/unittestenv/xdglocal/.keep
new file mode 100644 (file)
index 0000000..2f1d07a
--- /dev/null
@@ -0,0 +1 @@
+force git to include this file
diff --git a/agents/maildispatcher/maildispatcheragent.cpp b/agents/maildispatcher/maildispatcheragent.cpp
new file mode 100644 (file)
index 0000000..65b17bb
--- /dev/null
@@ -0,0 +1,361 @@
+/*
+    Copyright 2008 Ingo Klöcker <kloecker@kde.org>
+    Copyright 2009 Constantin Berzan <exit3219@gmail.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "maildispatcheragent.h"
+
+//#include "configdialog.h"
+#include "maildispatcheragentadaptor.h"
+#include "outboxqueue.h"
+#include "sendjob.h"
+#include "sentactionhandler.h"
+#include "settings.h"
+#include "settingsadaptor.h"
+
+#include <kdbusconnectionpool.h>
+#include <itemfetchscope.h>
+#include <mailtransport/sentactionattribute.h>
+
+#include <knotifyconfigwidget.h>
+#include "maildispatcher_debug.h"
+#include <KLocalizedString>
+#include <KMime/Message>
+#include <KNotification>
+#include <Kdelibs4ConfigMigrator>
+
+#include <QtCore/QTimer>
+#include <QtDBus/QDBusConnection>
+
+#ifdef MAIL_SERIALIZER_PLUGIN_STATIC
+
+Q_IMPORT_PLUGIN(akonadi_serializer_mail)
+#endif
+
+using namespace Akonadi;
+
+class MailDispatcherAgent::Private
+{
+public:
+    Private(MailDispatcherAgent *parent)
+        : q(parent),
+          queue(Q_NULLPTR),
+          currentJob(Q_NULLPTR),
+          aborting(false),
+          sendingInProgress(false),
+          sentAnything(false),
+          errorOccurred(false),
+          sentItemsSize(0),
+          sentActionHandler(Q_NULLPTR)
+    {
+    }
+
+    ~Private()
+    {
+    }
+
+    MailDispatcherAgent *const q;
+
+    OutboxQueue *queue;
+    SendJob *currentJob;
+    Item currentItem;
+    bool aborting;
+    bool sendingInProgress;
+    bool sentAnything;
+    bool errorOccurred;
+    qulonglong sentItemsSize;
+    SentActionHandler *sentActionHandler;
+
+    // Q_SLOTS:
+    void abort();
+    void dispatch();
+    void itemFetched(const Item &item);
+    void queueError(const QString &message);
+    void sendPercent(KJob *job, unsigned long percent);
+    void sendResult(KJob *job);
+    void emitStatusReady();
+};
+
+void MailDispatcherAgent::Private::abort()
+{
+    if (!q->isOnline()) {
+        qCDebug(MAILDISPATCHER_LOG) << "Offline. Ignoring call.";
+        return;
+    }
+
+    if (aborting) {
+        qCDebug(MAILDISPATCHER_LOG) << "Already aborting.";
+        return;
+    }
+
+    if (!sendingInProgress && queue->isEmpty()) {
+        qCDebug(MAILDISPATCHER_LOG) << "MDA is idle.";
+        Q_ASSERT(q->status() == AgentBase::Idle);
+    } else {
+        qCDebug(MAILDISPATCHER_LOG) << "Aborting...";
+        aborting = true;
+        if (currentJob) {
+            currentJob->abort();
+        }
+        // Further SendJobs will mark remaining items in the queue as 'aborted'.
+    }
+}
+
+void MailDispatcherAgent::Private::dispatch()
+{
+    Q_ASSERT(queue);
+
+    if (!q->isOnline() || sendingInProgress) {
+        qCDebug(MAILDISPATCHER_LOG) << "Offline or busy. See you later.";
+        return;
+    }
+
+    if (!queue->isEmpty()) {
+        if (!sentAnything) {
+            sentAnything = true;
+            sentItemsSize = 0;
+            Q_EMIT q->percent(0);
+        }
+        Q_EMIT q->status(AgentBase::Running,
+                         i18np("Sending messages (1 item in queue)...",
+                               "Sending messages (%1 items in queue)...", queue->count()));
+        qCDebug(MAILDISPATCHER_LOG) << "Attempting to dispatch the next message.";
+        sendingInProgress = true;
+        queue->fetchOne(); // will trigger itemFetched
+    } else {
+        qCDebug(MAILDISPATCHER_LOG) << "Empty queue.";
+        if (aborting) {
+            // Finished marking messages as 'aborted'.
+            aborting = false;
+            sentAnything = false;
+            Q_EMIT q->status(AgentBase::Idle, i18n("Sending canceled."));
+            QTimer::singleShot(3000, q, SLOT(emitStatusReady()));
+        } else {
+            if (sentAnything) {
+                // Finished sending messages in queue.
+                sentAnything = false;
+                Q_EMIT q->percent(100);
+                Q_EMIT q->status(AgentBase::Idle, i18n("Finished sending messages."));
+
+                if (!errorOccurred) {
+                    KNotification *notify = new KNotification(QStringLiteral("emailsent"));
+                    notify->setComponentName(QStringLiteral("akonadi_maildispatcher_agent"));
+                    notify->setTitle(i18nc("Notification title when email was sent", "E-Mail Successfully Sent"));
+                    notify->setText(i18nc("Notification when the email was sent", "Your E-Mail has been sent successfully."));
+                    notify->sendEvent();
+                }
+            } else {
+                // Empty queue.
+                Q_EMIT q->status(AgentBase::Idle, i18n("No items in queue."));
+            }
+            QTimer::singleShot(3000, q, SLOT(emitStatusReady()));
+        }
+
+        errorOccurred = false;
+    }
+}
+
+MailDispatcherAgent::MailDispatcherAgent(const QString &id)
+    : AgentBase(id),
+      d(new Private(this))
+{
+    Kdelibs4ConfigMigrator migrate(QStringLiteral("maildispatcheragent"));
+    migrate.setConfigFiles(QStringList() << QStringLiteral("maildispatcheragentrc") << QStringLiteral("akonadi_maildispatcher_agent.notifyrc"));
+    migrate.migrate();
+
+    qCDebug(MAILDISPATCHER_LOG) << "maildispatcheragent: At your service, sir!";
+#ifdef KDEPIM_STATIC_LIBS
+    ___MailTransport____INIT();
+#endif
+
+    new SettingsAdaptor(Settings::self());
+    new MailDispatcherAgentAdaptor(this);
+
+    KDBusConnectionPool::threadConnection().registerObject(QStringLiteral("/Settings"),
+            Settings::self(), QDBusConnection::ExportAdaptors);
+
+    KDBusConnectionPool::threadConnection().registerObject(QStringLiteral("/MailDispatcherAgent"),
+            this, QDBusConnection::ExportAdaptors);
+    KDBusConnectionPool::threadConnection().registerService(QStringLiteral("org.freedesktop.Akonadi.MailDispatcherAgent"));
+
+    d->queue = new OutboxQueue(this);
+    connect(d->queue, SIGNAL(newItems()),
+            this, SLOT(dispatch()));
+    connect(d->queue, SIGNAL(itemReady(Akonadi::Item)),
+            this, SLOT(itemFetched(Akonadi::Item)));
+    connect(d->queue, SIGNAL(error(QString)),
+            this, SLOT(queueError(QString)));
+    connect(this, SIGNAL(itemProcessed(Akonadi::Item,bool)),
+            d->queue, SLOT(itemProcessed(Akonadi::Item,bool)));
+    connect(this, SIGNAL(abortRequested()),
+            this, SLOT(abort()));
+
+    d->sentActionHandler = new SentActionHandler(this);
+
+    setNeedsNetwork(true);
+}
+
+MailDispatcherAgent::~MailDispatcherAgent()
+{
+    delete d;
+}
+
+void MailDispatcherAgent::configure(WId windowId)
+{
+    Q_UNUSED(windowId);
+    KNotifyConfigWidget::configure(Q_NULLPTR);
+}
+
+void MailDispatcherAgent::doSetOnline(bool online)
+{
+    Q_ASSERT(d->queue);
+    if (online) {
+        qCDebug(MAILDISPATCHER_LOG) << "Online. Dispatching messages.";
+        Q_EMIT status(AgentBase::Idle, i18n("Online, sending messages in queue."));
+        QTimer::singleShot(0, this, SLOT(dispatch()));
+    } else {
+        qCDebug(MAILDISPATCHER_LOG) << "Offline.";
+        Q_EMIT status(AgentBase::Idle, i18n("Offline, message sending suspended."));
+
+        // TODO: This way, the OutboxQueue will continue to react to changes in
+        // the outbox, but the MDA will just not send anything.  Is this what we
+        // want?
+    }
+
+    AgentBase::doSetOnline(online);
+}
+
+void MailDispatcherAgent::Private::itemFetched(const Item &item)
+{
+    qCDebug(MAILDISPATCHER_LOG) << "Fetched item" << item.id() << "; creating SendJob.";
+    Q_ASSERT(sendingInProgress);
+    Q_ASSERT(!currentItem.isValid());
+    currentItem = item;
+    Q_ASSERT(currentJob == 0);
+    Q_EMIT q->itemDispatchStarted();
+
+    currentJob = new SendJob(item, q);
+    if (aborting) {
+        currentJob->setMarkAborted();
+    }
+
+    q->status(AgentBase::Running, i18nc("Message with given subject is being sent.", "Sending: %1",
+                                        item.payload<KMime::Message::Ptr>()->subject()->asUnicodeString()));
+
+    connect(currentJob, SIGNAL(result(KJob*)),
+            q, SLOT(sendResult(KJob*)));
+    connect(currentJob, SIGNAL(percent(KJob*,ulong)),
+            q, SLOT(sendPercent(KJob*,ulong)));
+
+    currentJob->start();
+}
+
+void MailDispatcherAgent::Private::queueError(const QString &message)
+{
+    Q_EMIT q->error(message);
+    errorOccurred = true;
+    // FIXME figure out why this does not set the status to Broken, etc.
+}
+
+void MailDispatcherAgent::Private::sendPercent(KJob *job, unsigned long)
+{
+    Q_ASSERT(sendingInProgress);
+    Q_ASSERT(job == currentJob);
+    // The progress here is actually the TransportJob, not the entire SendJob,
+    // because the post-job doesn't report progress.  This should be fine,
+    // since the TransportJob is the lengthiest operation.
+
+    // Give the transport 80% of the weight, and move-to-sendmail 20%.
+    const double transportWeight = 0.8;
+
+    const int percent = 100 * (sentItemsSize + job->processedAmount(KJob::Bytes) * transportWeight)
+                        / (sentItemsSize + currentItem.size() + queue->totalSize());
+
+    qCDebug(MAILDISPATCHER_LOG) << "sentItemsSize" << sentItemsSize
+                                << "this job processed" << job->processedAmount(KJob::Bytes)
+                                << "queue totalSize" << queue->totalSize()
+                                << "total total size (sent+current+queue)" << (sentItemsSize + currentItem.size() + queue->totalSize())
+                                << "new percentage" << percent << "old percentage" << q->progress();
+
+    if (percent != q->progress()) {
+        // The progress can decrease too, if messages got added to the queue.
+        Q_EMIT q->percent(percent);
+    }
+
+    // It is possible that the number of queued messages has changed.
+    Q_EMIT q->status(AgentBase::Running,
+                     i18np("Sending messages (1 item in queue)...",
+                           "Sending messages (%1 items in queue)...", 1 + queue->count()));
+}
+
+void MailDispatcherAgent::Private::sendResult(KJob *job)
+{
+    Q_ASSERT(sendingInProgress);
+    Q_ASSERT(job == currentJob);
+    currentJob->disconnect(q);
+    currentJob = Q_NULLPTR;
+
+    Q_ASSERT(currentItem.isValid());
+    sentItemsSize += currentItem.size();
+    Q_EMIT q->itemProcessed(currentItem, !job->error());
+
+    const Akonadi::Item sentItem = currentItem;
+    currentItem = Item();
+
+    if (job->error()) {
+        // The SendJob gave the item an ErrorAttribute, so we don't have to
+        // do anything.
+        qCDebug(MAILDISPATCHER_LOG) << "Sending failed. error:" << job->errorString();
+
+        KNotification *notify = new KNotification(QStringLiteral("sendingfailed"));
+        notify->setComponentName(QStringLiteral("akonadi_maildispatcher_agent"));
+        notify->setTitle(i18nc("Notification title when email sending failed", "E-Mail Sending Failed"));
+        notify->setText(job->errorString());
+        notify->sendEvent();
+
+        errorOccurred = true;
+    } else {
+        qCDebug(MAILDISPATCHER_LOG) << "Sending succeeded.";
+
+        // handle possible sent actions
+        const MailTransport::SentActionAttribute *attribute = sentItem.attribute<MailTransport::SentActionAttribute>();
+        if (attribute) {
+            foreach (const MailTransport::SentActionAttribute::Action &action, attribute->actions()) {
+                sentActionHandler->runAction(action);
+            }
+        }
+    }
+
+    // dispatch next message
+    sendingInProgress = false;
+    QTimer::singleShot(0, q, SLOT(dispatch()));
+}
+
+void MailDispatcherAgent::Private::emitStatusReady()
+{
+    if (q->status() == AgentBase::Idle) {
+        // If still idle after aborting, clear 'aborted' status.
+        Q_EMIT q->status(AgentBase::Idle, i18n("Ready to dispatch messages."));
+    }
+}
+
+#ifndef KDEPIM_PLUGIN_AGENT
+AKONADI_AGENT_MAIN(MailDispatcherAgent)
+#endif
+
+#include "moc_maildispatcheragent.cpp"
diff --git a/agents/maildispatcher/maildispatcheragent.desktop b/agents/maildispatcher/maildispatcheragent.desktop
new file mode 100644 (file)
index 0000000..5fb619e
--- /dev/null
@@ -0,0 +1,93 @@
+[Desktop Entry]
+Name=Mail Dispatcher Agent
+Name[bs]=Dispačer mail agent
+Name[ca]=Agent distribuïdor de correu
+Name[ca@valencia]=Agent distribuïdor de correu
+Name[cs]=Agent odesílatele zpráv
+Name[da]=Mailafsendingsagent (MDA)
+Name[de]=Agent zur Nachrichten-Auslieferung
+Name[el]=Πράκτορας αποστολής αλληλογραφίας
+Name[en_GB]=Mail Dispatcher Agent
+Name[es]=Agente despachador de correo
+Name[et]=Kirjade edastamise agent
+Name[fi]=Postinvälitysagentti
+Name[fr]=Agent de diffusion de messages
+Name[gl]=Axente de Despacho de Correo
+Name[hu]=Levélfeladó ügynök
+Name[ia]=Agente Distributor de Posta
+Name[it]=Agente per la consegna della posta
+Name[ja]=メール送信エージェント
+Name[kk]=Пошта реттеуш агенті
+Name[km]=ភ្នាក់ងារ​កម្មវិធី​បញ្ជូន​សំបុត្រ
+Name[ko]=메일 가져오기 마법사
+Name[lt]=Laiškų gijų išdėstymo agentas
+Name[lv]=Pasta nosūtīšanas aģents
+Name[nb]=Agent for e-postsending
+Name[nds]=Nettpostverdeel-Hölper
+Name[nl]=Agent voor het verzenden van e-mail
+Name[pa]=ਮੇਲ ਡਿਸਪੈਚਰ ਏਜੰਟ
+Name[pl]=Agent przyjmowania poczty
+Name[pt]=Agente de Despacho do Correio
+Name[pt_BR]=Agente de encaminhamento de e-mails
+Name[ro]=Agent de livrare a mesajelor
+Name[ru]=Агент почтового диспетчера
+Name[sk]=Agent spracovania pošty
+Name[sl]=Posrednik za razpošiljanje pošte
+Name[sr]=Агент отпремања поште
+Name[sr@ijekavian]=Агент отпремања поште
+Name[sr@ijekavianlatin]=Agent otpremanja pošte
+Name[sr@latin]=Agent otpremanja pošte
+Name[sv]=E-postsändningsmodul
+Name[tr]=E-posta Yönetim Aracı
+Name[ug]=Mail Dispatcher Agent
+Name[uk]=Агент розподілу пошти
+Name[x-test]=xxMail Dispatcher Agentxx
+Name[zh_CN]=邮件签发代理
+Name[zh_TW]=郵件配送代理程式
+Comment=Dispatches email messages
+Comment[bs]=Raspoređuje E-mail poruke
+Comment[ca]=Distribueix missatges de correu electrònic
+Comment[ca@valencia]=Distribueix missatges de correu electrònic
+Comment[cs]=Odesílá e-mailové zprávy
+Comment[da]=Udsender e-mails
+Comment[de]=Verteilt E-Mail-Nachrichten
+Comment[el]=Διανέμει μηνύματα ηλ. ταχυδρομείου
+Comment[en_GB]=Dispatches email messages
+Comment[es]=Remite mensajes de correo
+Comment[et]=Kirjade edastamine
+Comment[fi]=Lähettää sähköpostiviestejä
+Comment[fr]=Diffuse les courriers électroniques
+Comment[gl]=Xestiona mensaxes de correo.
+Comment[hu]=E-mail üzeneteket kézbesít
+Comment[ia]=Expedi messages de e-posta
+Comment[it]=Invia messaggi di posta elettronica
+Comment[kk]=Пошта хаттарын үлестіру
+Comment[ko]=이메일 메시지를 가져옴
+Comment[lt]=Išsiunčia el. laiškus
+Comment[nb]=Sender ut e-postmeldinger
+Comment[nds]=Verdeelt Nettpost
+Comment[nl]=Verstuurt e-mailberichten
+Comment[pl]=Rozsyła wiadomości pocztowe
+Comment[pt]=Trata das mensagens de e-mail
+Comment[pt_BR]=Encaminhamento de e-mails
+Comment[ro]=Remite scrisori electronice
+Comment[ru]=Рассылает почтовые сообщения
+Comment[sk]=Vybavuje e-mailové správy
+Comment[sl]=Odpošlje e-poštna sporočila
+Comment[sr]=Отпрема е‑поштанске поруке
+Comment[sr@ijekavian]=Отпрема е‑поштанске поруке
+Comment[sr@ijekavianlatin]=Otprema e‑poštanske poruke
+Comment[sr@latin]=Otprema e‑poštanske poruke
+Comment[sv]=Skickar brev med e-post
+Comment[tr]=E-posta mesajlarını yollar
+Comment[uk]=Розповсюджує повідомлення електронної пошти
+Comment[x-test]=xxDispatches email messagesxx
+Comment[zh_CN]=发送电子邮件
+Comment[zh_TW]=分配電子郵件訊息
+Type=AkonadiAgent
+Exec=akonadi_maildispatcher_agent
+Icon=mail-folder-outbox
+
+X-Akonadi-MimeTypes=message/rfc822
+X-Akonadi-Capabilities=Unique,Autostart
+X-Akonadi-Identifier=akonadi_maildispatcher_agent
diff --git a/agents/maildispatcher/maildispatcheragent.h b/agents/maildispatcher/maildispatcheragent.h
new file mode 100644 (file)
index 0000000..3d944a8
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+    Copyright 2008 Ingo Klöcker <kloecker@kde.org>
+    Copyright 2009 Constantin Berzan <exit3219@gmail.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef MAILDISPATCHERAGENT_H
+#define MAILDISPATCHERAGENT_H
+
+#include <AgentBase>
+
+namespace Akonadi
+{
+class Item;
+}
+
+/**
+ * @short This agent dispatches mail put into the outbox collection.
+ */
+class MailDispatcherAgent : public Akonadi::AgentBase
+{
+    Q_OBJECT
+
+    Q_CLASSINFO("D-Bus Interface", "org.freedesktop.Akonadi.MailDispatcherAgent")
+
+public:
+    explicit MailDispatcherAgent(const QString &id);
+    ~MailDispatcherAgent();
+
+public Q_SLOTS:
+    void configure(WId windowId) Q_DECL_OVERRIDE;
+
+Q_SIGNALS:
+    /**
+     * Emitted when the MDA has attempted to send an item.
+     */
+    void itemProcessed(const Akonadi::Item &item, bool result);
+
+    /**
+     * Emitted when the MDA has begun processing an item
+     */
+    Q_SCRIPTABLE void itemDispatchStarted();
+
+protected:
+    void doSetOnline(bool online) Q_DECL_OVERRIDE;
+
+private:
+    //@cond PRIVATE
+    class Private;
+    Private *const d;
+
+    Q_PRIVATE_SLOT(d, void abort())
+    Q_PRIVATE_SLOT(d, void dispatch())
+    Q_PRIVATE_SLOT(d, void itemFetched(const Akonadi::Item &))
+    Q_PRIVATE_SLOT(d, void queueError(const QString &))
+    Q_PRIVATE_SLOT(d, void sendPercent(KJob *, unsigned long))
+    Q_PRIVATE_SLOT(d, void sendResult(KJob *))
+    Q_PRIVATE_SLOT(d, void emitStatusReady())
+    //@endcond
+};
+
+#endif // MAILDISPATCHERAGENT_H
diff --git a/agents/maildispatcher/maildispatcheragent.kcfg b/agents/maildispatcher/maildispatcheragent.kcfg
new file mode 100644 (file)
index 0000000..922fd13
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:kcfg="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
+      http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
+  <kcfgfile name="maildispatcheragentrc"/>
+  <group name="General">
+    <entry name="Outbox" type="LongLong">
+      <label>Outbox collection id</label>
+      <default>-1</default>
+    </entry>
+    <entry name="SentMail" type="LongLong">
+      <label>Sent Mail collection id</label>
+      <default>-1</default>
+    </entry>
+  </group>
+</kcfg>
diff --git a/agents/maildispatcher/org.freedesktop.Akonadi.MailDispatcherAgent.xml b/agents/maildispatcher/org.freedesktop.Akonadi.MailDispatcherAgent.xml
new file mode 100644 (file)
index 0000000..917af22
--- /dev/null
@@ -0,0 +1,7 @@
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<node>
+  <interface name="org.freedesktop.Akonadi.MailDispatcherAgent">
+    <signal name="itemDispatchStarted">
+    </signal>
+  </interface>
+</node>
diff --git a/agents/maildispatcher/outboxqueue.cpp b/agents/maildispatcher/outboxqueue.cpp
new file mode 100644 (file)
index 0000000..7a2c2cf
--- /dev/null
@@ -0,0 +1,455 @@
+/*
+    Copyright (c) 2009 Constantin Berzan <exit3219@gmail.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "outboxqueue.h"
+
+#include <QMultiMap>
+#include <QSet>
+#include <QTimer>
+
+#include "maildispatcher_debug.h"
+#include <KLocalizedString>
+
+#include <Attribute>
+#include <ItemFetchJob>
+#include <ItemFetchScope>
+#include <Monitor>
+#include <Akonadi/KMime/AddressAttribute>
+#include <Akonadi/KMime/MessageFlags>
+#include <Akonadi/KMime/SpecialMailCollections>
+#include <Akonadi/KMime/SpecialMailCollectionsRequestJob>
+
+#include <kmime/kmime_message.h>
+
+#include <mailtransport/dispatchmodeattribute.h>
+#include <mailtransport/sentbehaviourattribute.h>
+#include <mailtransport/transportattribute.h>
+
+using namespace Akonadi;
+using namespace MailTransport;
+
+static const int OUTBOX_DISCOVERY_RETRIES = 3; // number of times we try to find or create the outbox
+static const int OUTBOX_DISCOVERY_WAIT_TIME = 5000; // number of ms to wait before retrying
+
+/**
+ * @internal
+ */
+class OutboxQueue::Private
+{
+public:
+    Private(OutboxQueue *qq)
+        : q(qq),
+          outbox(-1),
+          monitor(Q_NULLPTR),
+          futureTimer(Q_NULLPTR),
+          totalSize(0),
+          outboxDiscoveryRetries(0)
+    {
+    }
+
+    OutboxQueue *const q;
+
+    Collection outbox;
+    Monitor *monitor;
+    QList<Item> queue;
+    QSet<Item> futureItems; // keeps track of items removed in the meantime
+    QMultiMap<QDateTime, Item> futureMap;
+    QTimer *futureTimer;
+    qulonglong totalSize;
+    int outboxDiscoveryRetries;
+
+#if 0
+    // If an item is modified externally between the moment we pass it to
+    // the MDA and the time the MDA marks it as sent, then we will get
+    // itemChanged() and may mistakenly re-add the item to the queue.
+    // So we ignore the item that we pass to the MDA, until the MDA finishes
+    // sending it.
+    Item currentItem;
+#endif
+    // HACK: The above is not enough.
+    // Apparently change notifications are delayed sometimes (???)
+    // and we re-add an item long after it was sent.  So keep a list of sent
+    // items.
+    // TODO debug and figure out why this happens.
+    QSet<Item::Id> ignore;
+
+    void initQueue();
+    void addIfComplete(const Item &item);
+
+    // Q_SLOTS:
+    void checkFuture();
+    void collectionFetched(KJob *job);
+    void itemFetched(KJob *job);
+    void localFoldersChanged();
+    void localFoldersRequestResult(KJob *job);
+    void itemAdded(const Item &item);
+    void itemChanged(const Item &item);
+    void itemMoved(const Item &item, const Collection &source, const Collection &dest);
+    void itemRemoved(const Item &item);
+    void itemProcessed(const Item &item, bool result);
+};
+
+void OutboxQueue::Private::initQueue()
+{
+    totalSize = 0;
+    queue.clear();
+
+    qCDebug(MAILDISPATCHER_LOG) << "Fetching items in collection" << outbox.id();
+    ItemFetchJob *job = new ItemFetchJob(outbox);
+    job->fetchScope().fetchAllAttributes();
+    job->fetchScope().fetchFullPayload(false);
+    connect(job, SIGNAL(result(KJob*)), q, SLOT(collectionFetched(KJob*)));
+}
+
+void OutboxQueue::Private::addIfComplete(const Item &item)
+{
+    if (ignore.contains(item.id())) {
+        qCDebug(MAILDISPATCHER_LOG) << "Item" << item.id() << "is ignored.";
+        return;
+    }
+
+    if (queue.contains(item)) {
+        qCDebug(MAILDISPATCHER_LOG) << "Item" << item.id() << "already in queue!";
+        return;
+    }
+
+    if (!item.hasAttribute<AddressAttribute>()) {
+        qCWarning(MAILDISPATCHER_LOG) << "Item" << item.id() << "does not have the required attribute Address.";
+        return;
+    }
+
+    if (!item.hasAttribute<DispatchModeAttribute>()) {
+        qCWarning(MAILDISPATCHER_LOG) << "Item" << item.id() << "does not have the required attribute DispatchMode.";
+        return;
+    }
+
+    if (!item.hasAttribute<SentBehaviourAttribute>()) {
+        qCWarning(MAILDISPATCHER_LOG) << "Item" << item.id() << "does not have the required attribute SentBehaviour.";
+        return;
+    }
+
+    if (!item.hasAttribute<TransportAttribute>()) {
+        qCWarning(MAILDISPATCHER_LOG) << "Item" << item.id() << "does not have the required attribute Transport.";
+        return;
+    }
+
+    if (!item.hasFlag(Akonadi::MessageFlags::Queued)) {
+        qCDebug(MAILDISPATCHER_LOG) << "Item" << item.id() << "has no '$QUEUED' flag.";
+        return;
+    }
+
+    const DispatchModeAttribute *dispatchModeAttribute = item.attribute<DispatchModeAttribute>();
+    Q_ASSERT(dispatchModeAttribute);
+    if (dispatchModeAttribute->dispatchMode() == DispatchModeAttribute::Manual) {
+        qCDebug(MAILDISPATCHER_LOG) << "Item" << item.id() << "is queued to be sent manually.";
+        return;
+    }
+
+    const TransportAttribute *transportAttribute = item.attribute<TransportAttribute>();
+    Q_ASSERT(transportAttribute);
+    if (transportAttribute->transport() == Q_NULLPTR) {
+        qCWarning(MAILDISPATCHER_LOG) << "Item" << item.id() << "has invalid transport.";
+        return;
+    }
+
+    const SentBehaviourAttribute *sentBehaviourAttribute = item.attribute<SentBehaviourAttribute>();
+    Q_ASSERT(sentBehaviourAttribute);
+    if (sentBehaviourAttribute->sentBehaviour() == SentBehaviourAttribute::MoveToCollection &&
+            !sentBehaviourAttribute->moveToCollection().isValid()) {
+        qCWarning(MAILDISPATCHER_LOG) << "Item" << item.id() << "has invalid sent-mail collection.";
+        return;
+    }
+
+    // This check requires fetchFullPayload. -> slow (?)
+    /*
+    if ( !item.hasPayload<KMime::Message::Ptr>() ) {
+      qCWarning(MAILDISPATCHER_LOG) << "Item" << item.id() << "does not have KMime::Message::Ptr payload.";
+      return;
+    }
+    */
+
+    if (dispatchModeAttribute->dispatchMode() == DispatchModeAttribute::Automatic &&
+            dispatchModeAttribute->sendAfter().isValid() &&
+            dispatchModeAttribute->sendAfter() > QDateTime::currentDateTime()) {
+        // All the above was OK, so accept it for the future.
+        qCDebug(MAILDISPATCHER_LOG) << "Item" << item.id() << "is accepted to be sent in the future.";
+        futureMap.insert(dispatchModeAttribute->sendAfter(), item);
+        Q_ASSERT(!futureItems.contains(item));
+        futureItems.insert(item);
+        checkFuture();
+        return;
+    }
+
+    qCDebug(MAILDISPATCHER_LOG) << "Item" << item.id() << "is accepted into the queue (size" << item.size() << ").";
+    Q_ASSERT(!queue.contains(item));
+    totalSize += item.size();
+    queue.append(item);
+    Q_EMIT q->newItems();
+}
+
+void OutboxQueue::Private::checkFuture()
+{
+    qCDebug(MAILDISPATCHER_LOG) << "The future is here." << futureMap.count() << "items in futureMap.";
+    Q_ASSERT(futureTimer);
+    futureTimer->stop();
+    // By default, re-check in one hour.
+    futureTimer->setInterval(60 * 60 * 1000);
+
+    // Check items in ascending order of date.
+    while (!futureMap.isEmpty()) {
+        QMap<QDateTime, Item>::iterator it = futureMap.begin();
+        qCDebug(MAILDISPATCHER_LOG) << "Item with due date" << it.key();
+        if (it.key() > QDateTime::currentDateTime()) {
+            const int secs = QDateTime::currentDateTime().secsTo(it.key()) + 1;
+            qCDebug(MAILDISPATCHER_LOG) << "Future, in" << secs << "seconds.";
+            Q_ASSERT(secs >= 0);
+            if (secs < 60 * 60) {
+                futureTimer->setInterval(secs * 1000);
+            }
+            break; // all others are in the future too
+        }
+        if (!futureItems.contains(it.value())) {
+            qCDebug(MAILDISPATCHER_LOG) << "Item disappeared.";
+        } else {
+            qCDebug(MAILDISPATCHER_LOG) << "Due date is here. Queuing.";
+            addIfComplete(it.value());
+            futureItems.remove(it.value());
+        }
+        futureMap.erase(it);
+    }
+
+    qCDebug(MAILDISPATCHER_LOG) << "Timer set to checkFuture again in" << futureTimer->interval() / 1000 << "seconds"
+                                << "(that is" << futureTimer->interval() / 1000 / 60 << "minutes).";
+
+    futureTimer->start();
+}
+
+void OutboxQueue::Private::collectionFetched(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(MAILDISPATCHER_LOG) << "Failed to fetch outbox collection.  Queue will be empty until the outbox changes.";
+        return;
+    }
+
+    const ItemFetchJob *fetchJob = qobject_cast<ItemFetchJob *>(job);
+    Q_ASSERT(fetchJob);
+    qCDebug(MAILDISPATCHER_LOG) << "Fetched" << fetchJob->items().count() << "items.";
+
+    foreach (const Item &item, fetchJob->items()) {
+        addIfComplete(item);
+    }
+}
+
+void OutboxQueue::Private::itemFetched(KJob *job)
+{
+    if (job->error()) {
+        qCDebug(MAILDISPATCHER_LOG) << "Error fetching item:" << job->errorString() << ". Trying next item in queue.";
+        q->fetchOne();
+    }
+
+    const ItemFetchJob *fetchJob = qobject_cast<ItemFetchJob *>(job);
+    Q_ASSERT(fetchJob);
+    if (fetchJob->items().count() != 1) {
+        qCDebug(MAILDISPATCHER_LOG) << "Fetched" << fetchJob->items().count() << ", expected 1. Trying next item in queue.";
+        q->fetchOne();
+    }
+
+    if (!fetchJob->items().isEmpty()) {
+        Q_EMIT q->itemReady(fetchJob->items().at(0));
+    }
+}
+
+void OutboxQueue::Private::localFoldersChanged()
+{
+    // Called on startup, and whenever the local folders change.
+
+    if (SpecialMailCollections::self()->hasDefaultCollection(SpecialMailCollections::Outbox)) {
+        // Outbox is ready, init the queue from it.
+        const Collection collection = SpecialMailCollections::self()->defaultCollection(SpecialMailCollections::Outbox);
+        Q_ASSERT(collection.isValid());
+
+        if (outbox != collection) {
+            monitor->setCollectionMonitored(outbox, false);
+            monitor->setCollectionMonitored(collection, true);
+            outbox = collection;
+            qCDebug(MAILDISPATCHER_LOG) << "Changed outbox to" << outbox.id();
+            initQueue();
+        }
+    } else {
+        // Outbox is not ready. Request it, since otherwise we will not know when
+        // new messages appear.
+        // (Note that we are a separate process, so we get no notification when
+        // MessageQueueJob requests the Outbox.)
+        monitor->setCollectionMonitored(outbox, false);
+        outbox = Collection(-1);
+
+        SpecialMailCollectionsRequestJob *job = new SpecialMailCollectionsRequestJob(q);
+        job->requestDefaultCollection(SpecialMailCollections::Outbox);
+        connect(job, SIGNAL(result(KJob*)), q, SLOT(localFoldersRequestResult(KJob*)));
+
+        qCDebug(MAILDISPATCHER_LOG) << "Requesting outbox folder.";
+        job->start();
+    }
+
+    // make sure we have a place to dump the sent mails as well
+    if (!SpecialMailCollections::self()->hasDefaultCollection(SpecialMailCollections::SentMail)) {
+        SpecialMailCollectionsRequestJob *job = new SpecialMailCollectionsRequestJob(q);
+        job->requestDefaultCollection(SpecialMailCollections::SentMail);
+
+        qCDebug(MAILDISPATCHER_LOG) << "Requesting sent-mail folder";
+        job->start();
+    }
+}
+
+void OutboxQueue::Private::localFoldersRequestResult(KJob *job)
+{
+    if (job->error()) {
+        // We tried to create the outbox, but that failed. This could be because some
+        // other process, the mail app, for example, tried to create it at the
+        // same time. So try again, once or twice, but wait a little in between, longer
+        // each time. If we still haven't managed to create it after a few retries,
+        // error hard.
+
+        if (++outboxDiscoveryRetries <= OUTBOX_DISCOVERY_RETRIES) {
+            const int timeout = OUTBOX_DISCOVERY_WAIT_TIME * outboxDiscoveryRetries;
+            qCWarning(MAILDISPATCHER_LOG) << "Failed to get outbox folder. Retrying in: " << timeout;
+            QTimer::singleShot(timeout, q, SLOT(localFoldersChanged()));
+        } else {
+            qCWarning(MAILDISPATCHER_LOG) << "Failed to get outbox folder. Giving up.";;
+            Q_EMIT q->error(i18n("Could not access the outbox folder (%1).", job->errorString()));
+        }
+        return;
+    }
+
+    localFoldersChanged();
+}
+
+void OutboxQueue::Private::itemAdded(const Item &item)
+{
+    addIfComplete(item);
+}
+
+void OutboxQueue::Private::itemChanged(const Item &item)
+{
+    addIfComplete(item);
+    // TODO: if the item is moved out of the outbox, will I get itemChanged?
+}
+
+void OutboxQueue::Private::itemMoved(const Item &item, const Collection &source, const Collection &destination)
+{
+    if (source == outbox) {
+        itemRemoved(item);
+    } else if (destination == outbox) {
+        addIfComplete(item);
+    }
+}
+
+void OutboxQueue::Private::itemRemoved(const Item &removedItem)
+{
+    // @p item has size=0, so get the size from our own copy.
+    const int index = queue.indexOf(removedItem);
+    if (index == -1) {
+        // Item was not in queue at all.
+        return;
+    }
+
+    Item item(queue.takeAt(index));
+    qCDebug(MAILDISPATCHER_LOG) << "Item" << item.id() << "(size" << item.size() << ") was removed from the queue.";
+    totalSize -= item.size();
+
+    futureItems.remove(removedItem);
+}
+
+void OutboxQueue::Private::itemProcessed(const Item &item, bool result)
+{
+    Q_ASSERT(ignore.contains(item.id()));
+    if (!result) {
+        // Give the user a chance to re-send the item if it failed.
+        ignore.remove(item.id());
+    }
+}
+
+OutboxQueue::OutboxQueue(QObject *parent)
+    : QObject(parent),
+      d(new Private(this))
+{
+    d->monitor = new Monitor(this);
+    d->monitor->itemFetchScope().fetchAllAttributes();
+    d->monitor->itemFetchScope().fetchFullPayload(false);
+    connect(d->monitor, SIGNAL(itemAdded(Akonadi::Item,Akonadi::Collection)),
+            this, SLOT(itemAdded(Akonadi::Item)));
+    connect(d->monitor, SIGNAL(itemChanged(Akonadi::Item,QSet<QByteArray>)),
+            this, SLOT(itemChanged(Akonadi::Item)));
+    connect(d->monitor, SIGNAL(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection)),
+            this, SLOT(itemMoved(Akonadi::Item,Akonadi::Collection,Akonadi::Collection)));
+    connect(d->monitor, SIGNAL(itemRemoved(Akonadi::Item)),
+            this, SLOT(itemRemoved(Akonadi::Item)));
+
+    connect(SpecialMailCollections::self(), SIGNAL(defaultCollectionsChanged()), this, SLOT(localFoldersChanged()));
+    d->localFoldersChanged();
+
+    d->futureTimer = new QTimer(this);
+    connect(d->futureTimer, SIGNAL(timeout()), this, SLOT(checkFuture()));
+    d->futureTimer->start(60 * 60 * 1000);   // 1 hour
+}
+
+OutboxQueue::~OutboxQueue()
+{
+    delete d;
+}
+
+bool OutboxQueue::isEmpty() const
+{
+    return d->queue.isEmpty();
+}
+
+int OutboxQueue::count() const
+{
+    if (d->queue.count() == 0) {
+        // TODO Is this asking for too much?
+        Q_ASSERT(d->totalSize == 0);
+    }
+    return d->queue.count();
+}
+
+qulonglong OutboxQueue::totalSize() const
+{
+    return d->totalSize;
+}
+
+void OutboxQueue::fetchOne()
+{
+    if (isEmpty()) {
+        qCDebug(MAILDISPATCHER_LOG) << "Empty queue.";
+        return;
+    }
+
+    const Item item = d->queue.takeFirst();
+
+    d->totalSize -= item.size();
+    Q_ASSERT(!d->ignore.contains(item.id()));
+    d->ignore.insert(item.id());
+
+    ItemFetchJob *job = new ItemFetchJob(item);
+    job->fetchScope().fetchAllAttributes();
+    job->fetchScope().fetchFullPayload();
+    connect(job, SIGNAL(result(KJob*)), this, SLOT(itemFetched(KJob*)));
+}
+
+#include "moc_outboxqueue.cpp"
diff --git a/agents/maildispatcher/outboxqueue.h b/agents/maildispatcher/outboxqueue.h
new file mode 100644 (file)
index 0000000..721c5b4
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+    Copyright (c) 2009 Constantin Berzan <exit3219@gmail.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OUTBOXQUEUE_H
+#define OUTBOXQUEUE_H
+
+#include <Collection>
+#include <Item>
+
+#include <QObject>
+
+class KJob;
+
+/**
+ * @short Monitors the outbox collection and provides a queue of messages for the MDA to send.
+ */
+class OutboxQueue : public QObject
+{
+    Q_OBJECT
+    friend class MailDispatcherAgent;
+
+public:
+    /**
+     * Creates a new outbox queue.
+     *
+     * @param parent The parent object.
+     */
+    explicit OutboxQueue(QObject *parent = Q_NULLPTR);
+
+    /**
+     * Destroys the outbox queue.
+     */
+    virtual ~OutboxQueue();
+
+    /**
+     * Returns whether the queue is empty.
+     */
+    bool isEmpty() const;
+
+    /**
+     * Returns the number of items in the queue.
+     */
+    int count() const;
+
+    /**
+     * Returns the size (in bytes) of all items in the queue.
+     */
+    qulonglong totalSize() const;
+
+    /**
+     * Fetches an item and emits itemReady() when done.
+     */
+    void fetchOne();
+
+Q_SIGNALS:
+    void itemReady(const Akonadi::Item &item);
+    void newItems();
+    void error(const QString &error);
+
+private:
+    //@cond PRIVATE
+    class Private;
+    Private *const d;
+
+    Q_PRIVATE_SLOT(d, void checkFuture())
+    Q_PRIVATE_SLOT(d, void collectionFetched(KJob *))
+    Q_PRIVATE_SLOT(d, void itemFetched(KJob *))
+    Q_PRIVATE_SLOT(d, void localFoldersChanged())
+    Q_PRIVATE_SLOT(d, void localFoldersRequestResult(KJob *))
+    Q_PRIVATE_SLOT(d, void itemAdded(Akonadi::Item))
+    Q_PRIVATE_SLOT(d, void itemChanged(Akonadi::Item))
+    Q_PRIVATE_SLOT(d, void itemMoved(Akonadi::Item, Akonadi::Collection, Akonadi::Collection))
+    Q_PRIVATE_SLOT(d, void itemRemoved(Akonadi::Item))
+    Q_PRIVATE_SLOT(d, void itemProcessed(Akonadi::Item, bool))
+    //@endcond
+};
+
+#endif
diff --git a/agents/maildispatcher/sendjob.cpp b/agents/maildispatcher/sendjob.cpp
new file mode 100644 (file)
index 0000000..d996c6e
--- /dev/null
@@ -0,0 +1,486 @@
+/*
+    Copyright (c) 2009 Constantin Berzan <exit3219@gmail.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "sendjob.h"
+
+#include "storeresultjob.h"
+
+#include <agentinstance.h>
+#include <agentmanager.h>
+#include <collection.h>
+#include <kdbusconnectionpool.h>
+#include <item.h>
+#include <itemdeletejob.h>
+#include <itemmodifyjob.h>
+#include <itemmovejob.h>
+#include <collectionfetchjob.h>
+#include <Akonadi/KMime/AddressAttribute>
+#include <Akonadi/KMime/MessageParts>
+#include <Akonadi/KMime/SpecialMailCollections>
+#include <transportresourcebase.h>
+#include "maildispatcher_debug.h"
+#include <klocalizedstring.h>
+#include <kmime/kmime_message.h>
+#include <mailtransport/sentbehaviourattribute.h>
+#include <mailtransport/transport.h>
+#include <mailtransport/transportattribute.h>
+#include <mailtransport/transportjob.h>
+#include <mailtransport/transportmanager.h>
+
+#include <QtCore/QTimer>
+#include <QtDBus/QDBusInterface>
+#include <QtDBus/QDBusReply>
+
+using namespace Akonadi;
+using namespace KMime;
+using namespace MailTransport;
+
+/**
+ * Private class that helps to provide binary compatibility between releases.
+ * @internal
+ */
+class SendJob::Private
+{
+public:
+    Private(const Item &itm, SendJob *qq)
+        : q(qq),
+          item(itm),
+          currentJob(Q_NULLPTR),
+          interface(Q_NULLPTR),
+          mailfilterInterface(Q_NULLPTR),
+          aborting(false)
+    {
+    }
+
+    SendJob *const q;
+    Item item;
+    KJob *currentJob;
+    QString resourceId;
+    QDBusInterface *interface;
+    QDBusInterface *mailfilterInterface;
+    bool aborting;
+
+    void doAkonadiTransport();
+    void doTraditionalTransport();
+    void doPostJob(bool transportSuccess, const QString &transportMessage);
+    void storeResult(bool success, const QString &message = QString());
+    void abortPostJob();
+    bool filterItem(int filterset);
+
+    // slots
+    void doTransport();
+    void transportPercent(KJob *job, unsigned long percent);
+    void transportResult(KJob *job);
+    void resourceProgress(const AgentInstance &instance);
+    void resourceResult(qlonglong itemId, int result, const QString &message);
+    void postJobResult(KJob *job);
+    void doEmitResult(KJob *job);
+    void slotSentMailCollectionFetched(KJob *job);
+};
+
+void SendJob::Private::doTransport()
+{
+    qCDebug(MAILDISPATCHER_LOG) << "Transporting message.";
+
+    if (aborting) {
+        qCDebug(MAILDISPATCHER_LOG) << "Marking message as aborted.";
+        q->setError(UserDefinedError);
+        q->setErrorText(i18n("Message sending aborted."));
+        storeResult(false, i18n("Message sending aborted."));
+        return;
+    }
+
+    // Is it an Akonadi transport or a traditional one?
+    const TransportAttribute *transportAttribute = item.attribute<TransportAttribute>();
+    Q_ASSERT(transportAttribute);
+    if (!transportAttribute->transport()) {
+        storeResult(false, i18n("Could not initiate message transport. Possibly invalid transport."));
+        return;
+    }
+
+    const TransportType type = transportAttribute->transport()->transportType();
+    if (!type.isValid()) {
+        storeResult(false, i18n("Could not send message. Invalid transport."));
+        return;
+    }
+
+    if (!filterItem(8)) {   //BeforeOutbound
+        return;
+    }
+
+    if (type.type() == Transport::EnumType::Akonadi) {
+        // Send the item directly to the resource that will send it.
+        resourceId = transportAttribute->transport()->host();
+        doAkonadiTransport();
+    } else {
+        // Use a traditional transport job.
+        doTraditionalTransport();
+    }
+}
+
+void SendJob::Private::doAkonadiTransport()
+{
+    Q_ASSERT(!resourceId.isEmpty());
+    Q_ASSERT(interface == 0);
+
+    interface = new QDBusInterface(
+        QLatin1String("org.freedesktop.Akonadi.Resource.") + resourceId,
+        QStringLiteral("/Transport"), QStringLiteral("org.freedesktop.Akonadi.Resource.Transport"),
+        KDBusConnectionPool::threadConnection(), q);
+
+    if (!interface->isValid()) {
+        storeResult(false, i18n("Failed to get D-Bus interface of resource %1.", resourceId));
+        delete interface;
+        interface = Q_NULLPTR;
+        return;
+    }
+
+    // Signals.
+    QObject::connect(AgentManager::self(), SIGNAL(instanceProgressChanged(Akonadi::AgentInstance)),
+                     q, SLOT(resourceProgress(Akonadi::AgentInstance)));
+    QObject::connect(interface, SIGNAL(transportResult(qlonglong,int,QString)),
+                     q, SLOT(resourceResult(qlonglong,int,QString)));
+
+    // Start sending.
+    const QDBusReply<void> reply = interface->call(QStringLiteral("send"), item.id());
+    if (!reply.isValid()) {
+        storeResult(false, i18n("Invalid D-Bus reply from resource %1.", resourceId));
+        return;
+    }
+}
+
+void SendJob::Private::doTraditionalTransport()
+{
+    const TransportAttribute *transportAttribute = item.attribute<TransportAttribute>();
+    TransportJob *job = TransportManager::self()->createTransportJob(transportAttribute->transportId());
+
+    Q_ASSERT(job);
+    Q_ASSERT(currentJob == 0);
+
+    currentJob = job;
+
+    // Message.
+    Q_ASSERT(item.hasPayload<Message::Ptr>());
+    const Message::Ptr message = item.payload<Message::Ptr>();
+    bool needAssemble = false;
+    if (message->hasHeader("Bcc")) {
+        message->removeHeader("Bcc");
+        needAssemble = true;
+    }
+    if (message->hasHeader("X-KMail-Identity")) {
+        message->removeHeader("X-KMail-Identity");
+        needAssemble = true;
+    }
+    if (message->hasHeader("X-KMail-Dictionary")) {
+        message->removeHeader("X-KMail-Dictionary");
+        needAssemble = true;
+    }
+
+    if (needAssemble) {
+        message->assemble();
+    }
+    const QByteArray content = message->encodedContent(true) + "\r\n";
+    Q_ASSERT(!content.isEmpty());
+
+    // Addresses.
+    const AddressAttribute *addressAttribute = item.attribute<AddressAttribute>();
+    Q_ASSERT(addressAttribute);
+
+    job->setData(content);
+    job->setSender(addressAttribute->from());
+    job->setTo(addressAttribute->to());
+    job->setCc(addressAttribute->cc());
+    job->setBcc(addressAttribute->bcc());
+
+    // Signals.
+    connect(job, SIGNAL(result(KJob*)),
+            q, SLOT(transportResult(KJob*)));
+    connect(job, SIGNAL(percent(KJob*,ulong)),
+            q, SLOT(transportPercent(KJob*,ulong)));
+
+    job->start();
+}
+
+void SendJob::Private::transportPercent(KJob *job, unsigned long)
+{
+    Q_ASSERT(currentJob == job);
+    qCDebug(MAILDISPATCHER_LOG) << "Processed amount" << job->processedAmount(KJob::Bytes)
+                                << "total amount" << job->totalAmount(KJob::Bytes);
+
+    q->setTotalAmount(KJob::Bytes, job->totalAmount(KJob::Bytes));     // Is not set at the time of start().
+    q->setProcessedAmount(KJob::Bytes, job->processedAmount(KJob::Bytes));
+}
+
+void SendJob::Private::transportResult(KJob *job)
+{
+    Q_ASSERT(currentJob == job);
+    currentJob = Q_NULLPTR;
+    doPostJob(!job->error(), job->errorString());
+}
+
+void SendJob::Private::resourceProgress(const AgentInstance &instance)
+{
+    if (!interface) {
+        // We might have gotten a very late signal.
+        qCWarning(MAILDISPATCHER_LOG) << "called but no resource job running!";
+        return;
+    }
+
+    if (instance.identifier() == resourceId) {
+        // This relies on the resource's progress representing the progress of
+        // sending this item.
+        q->setPercent(instance.progress());
+    }
+}
+
+void SendJob::Private::resourceResult(qlonglong itemId, int result,
+                                      const QString &message)
+{
+    Q_UNUSED(itemId);
+    Q_ASSERT(interface);
+    delete interface; // So that abort() knows the transport job is over.
+    interface = Q_NULLPTR;
+
+    const TransportResourceBase::TransportResult transportResult =
+        static_cast<TransportResourceBase::TransportResult>(result);
+
+    const bool success = (transportResult == TransportResourceBase::TransportSucceeded);
+
+    Q_ASSERT(itemId == item.id());
+    doPostJob(success, message);
+}
+
+void SendJob::Private::abortPostJob()
+{
+    // We were unlucky and LocalFolders is recreating its stuff right now.
+    // We will not wait for it.
+    qCWarning(MAILDISPATCHER_LOG) << "Default sent mail collection unavailable, not moving the mail after sending.";
+    q->setError(UserDefinedError);
+    q->setErrorText(i18n("Default sent-mail folder unavailable. Keeping message in outbox."));
+    storeResult(false, q->errorString());
+}
+
+void SendJob::Private::doPostJob(bool transportSuccess, const QString &transportMessage)
+{
+    qCDebug(MAILDISPATCHER_LOG) << "success" << transportSuccess << "message" << transportMessage;
+
+    if (!transportSuccess) {
+        qCDebug(MAILDISPATCHER_LOG) << "Error transporting.";
+        q->setError(UserDefinedError);
+
+        const QString error = aborting ? i18n("Message transport aborted.")
+                              : i18n("Failed to transport message.");
+
+        q->setErrorText(error + QLatin1Char(' ') + transportMessage);
+        storeResult(false, q->errorString());
+    } else {
+        qCDebug(MAILDISPATCHER_LOG) << "Success transporting.";
+
+        // Delete or move to sent-mail.
+        const SentBehaviourAttribute *attribute = item.attribute<SentBehaviourAttribute>();
+        Q_ASSERT(attribute);
+
+        if (attribute->sentBehaviour() == SentBehaviourAttribute::Delete) {
+            qCDebug(MAILDISPATCHER_LOG) << "Deleting item from outbox.";
+            currentJob = new ItemDeleteJob(item);
+            QObject::connect(currentJob, SIGNAL(result(KJob*)), q, SLOT(postJobResult(KJob*)));
+        } else {
+            if (attribute->sentBehaviour() == SentBehaviourAttribute::MoveToDefaultSentCollection) {
+                if (SpecialMailCollections::self()->hasDefaultCollection(SpecialMailCollections::SentMail)) {
+                    currentJob = new ItemMoveJob(item, SpecialMailCollections::self()->defaultCollection(SpecialMailCollections::SentMail), q);
+                    QObject::connect(currentJob, SIGNAL(result(KJob*)), q, SLOT(postJobResult(KJob*)));
+                } else {
+                    abortPostJob();
+                }
+            } else {
+                qCDebug(MAILDISPATCHER_LOG) << "sentBehaviour=" << attribute->sentBehaviour() << "using collection from attribute";
+                currentJob = new CollectionFetchJob(attribute->moveToCollection(), Akonadi::CollectionFetchJob::Base);
+                QObject::connect(currentJob, SIGNAL(result(KJob*)),
+                                 q, SLOT(slotSentMailCollectionFetched(KJob*)));
+
+            }
+        }
+    }
+}
+
+bool SendJob::Private::filterItem(int filterset)
+{
+    Q_ASSERT(mailfilterInterface == 0);
+
+    // TODO: create on stack
+    mailfilterInterface = new QDBusInterface(
+        QStringLiteral("org.freedesktop.Akonadi.MailFilterAgent"),
+        QStringLiteral("/MailFilterAgent"), QStringLiteral("org.freedesktop.Akonadi.MailFilterAgent"),
+        KDBusConnectionPool::threadConnection(), q);
+
+    if (!mailfilterInterface->isValid()) {
+        storeResult(false, i18n("Failed to get D-Bus interface of mailfilteragent."));
+        delete mailfilterInterface;
+        mailfilterInterface = Q_NULLPTR;
+        return false;
+    }
+
+    //Outbound = 0x2
+    const QDBusReply<void> reply = mailfilterInterface->call(QStringLiteral("filterItem"), item.id(), filterset, QString());
+    if (!reply.isValid()) {
+        storeResult(false, i18n("Invalid D-Bus reply from mailfilteragent"));
+        delete mailfilterInterface;
+        mailfilterInterface = Q_NULLPTR;
+        return false;
+    }
+
+    delete mailfilterInterface;
+    mailfilterInterface = Q_NULLPTR;
+    return true;
+}
+
+void SendJob::Private::slotSentMailCollectionFetched(KJob *job)
+{
+    Akonadi::Collection fetchCol;
+    bool ok = false;
+    if (!job->error()) {
+        const CollectionFetchJob *const fetchJob = qobject_cast<CollectionFetchJob *>(job);
+        if (!fetchJob->collections().isEmpty()) {
+            fetchCol = fetchJob->collections().at(0);
+            ok = true;
+        }
+    }
+    if (!ok) {
+        if (!SpecialMailCollections::self()->hasDefaultCollection(SpecialMailCollections::SentMail)) {
+            abortPostJob();
+            return;
+        }
+        fetchCol = SpecialMailCollections::self()->defaultCollection(SpecialMailCollections::SentMail);
+    }
+    currentJob = new ItemMoveJob(item, fetchCol, q);
+    QObject::connect(currentJob, SIGNAL(result(KJob*)), q, SLOT(postJobResult(KJob*)));
+}
+
+void SendJob::Private::postJobResult(KJob *job)
+{
+    Q_ASSERT(currentJob == job);
+    currentJob = Q_NULLPTR;
+    const SentBehaviourAttribute *attribute = item.attribute<SentBehaviourAttribute>();
+    Q_ASSERT(attribute);
+
+    if (job->error()) {
+        qCDebug(MAILDISPATCHER_LOG) << "Error deleting or moving to sent-mail.";
+
+        QString errorString;
+        switch (attribute->sentBehaviour()) {
+        case SentBehaviourAttribute::Delete:
+            errorString =
+                i18n("Sending succeeded, but failed to remove the message from the outbox.");
+            break;
+        default:
+            errorString =
+                i18n("Sending succeeded, but failed to move the message to the sent-mail folder.");
+            break;
+        }
+        q->setError(UserDefinedError);
+        q->setErrorText(errorString + QLatin1Char(' ') + job->errorString());
+        storeResult(false, q->errorString());
+    } else {
+        qCDebug(MAILDISPATCHER_LOG) << "Success deleting or moving to sent-mail.";
+        if (!filterItem(2)) {   //Outbound
+            return;
+        }
+        if (attribute->sentBehaviour() == SentBehaviourAttribute::Delete) {
+            q->emitResult();
+        } else {
+            storeResult(true);
+        }
+    }
+}
+
+void SendJob::Private::storeResult(bool success, const QString &message)
+{
+    qCDebug(MAILDISPATCHER_LOG) << "success" << success << "message" << message;
+
+    Q_ASSERT(currentJob == 0);
+    currentJob = new StoreResultJob(item, success, message);
+    connect(currentJob, SIGNAL(result(KJob*)), q, SLOT(doEmitResult(KJob*)));
+}
+
+void SendJob::Private::doEmitResult(KJob *job)
+{
+    Q_ASSERT(currentJob == job);
+    currentJob = Q_NULLPTR;
+
+    if (job->error()) {
+        qCWarning(MAILDISPATCHER_LOG) << "Error storing result.";
+        q->setError(UserDefinedError);
+        q->setErrorText(q->errorString() + QLatin1Char(' ') + i18n("Failed to store result in item.") + QLatin1Char(' ') + job->errorString());
+    } else {
+        qCDebug(MAILDISPATCHER_LOG) << "Success storing result.";
+        // It is still possible that the transport failed.
+        StoreResultJob *srJob = static_cast<StoreResultJob *>(job);
+        if (!srJob->success()) {
+            q->setError(UserDefinedError);
+            q->setErrorText(srJob->message());
+        }
+    }
+
+    q->emitResult();
+}
+
+SendJob::SendJob(const Item &item, QObject *parent)
+    : KJob(parent),
+      d(new Private(item, this))
+{
+}
+
+SendJob::~SendJob()
+{
+    delete d;
+}
+
+void SendJob::start()
+{
+    QTimer::singleShot(0, this, SLOT(doTransport()));
+}
+
+void SendJob::setMarkAborted()
+{
+    Q_ASSERT(!d->aborting);
+    d->aborting = true;
+}
+
+void SendJob::abort()
+{
+    setMarkAborted();
+
+    if (dynamic_cast<TransportJob *>(d->currentJob)) {
+        qCDebug(MAILDISPATCHER_LOG) << "Abort called, active transport job.";
+        // Abort transport.
+        d->currentJob->kill(KJob::EmitResult);
+    } else if (d->interface != Q_NULLPTR) {
+        qCDebug(MAILDISPATCHER_LOG) << "Abort called, propagating to resource.";
+        // Abort resource doing transport.
+        AgentInstance instance = AgentManager::self()->instance(d->resourceId);
+        instance.abortCurrentTask();
+    } else {
+        qCDebug(MAILDISPATCHER_LOG) << "Abort called, but no transport job is active.";
+        // Either transport has not started, in which case doTransport will
+        // mark the item as aborted, or the item has already been sent, in which
+        // case there is nothing we can do.
+    }
+}
+
+#include "moc_sendjob.cpp"
diff --git a/agents/maildispatcher/sendjob.h b/agents/maildispatcher/sendjob.h
new file mode 100644 (file)
index 0000000..b2137ba
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+    Copyright (c) 2009 Constantin Berzan <exit3219@gmail.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef SENDJOB_H
+#define SENDJOB_H
+
+#include <KJob>
+
+namespace Akonadi
+{
+class Item;
+}
+
+/**
+ * @short A job to send a mail
+ *
+ * This class takes a prevalidated Item with all the required attributes,
+ * sends it using MailTransport, and then stores the result of the sending
+ * operation in the item.
+ */
+class SendJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new send job.
+     *
+     * @param item The item to send.
+     * @param parent The parent object.
+     */
+    explicit SendJob(const Akonadi::Item &item, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Destroys the send job.
+     */
+    virtual ~SendJob();
+
+    /**
+     * Starts the job.
+     */
+    void start() Q_DECL_OVERRIDE;
+
+    /**
+     * If this function is called before the job is started, the SendJob will
+     * just mark the item as aborted, instead of sending it.
+     * Do not call this function more than once.
+     */
+    void setMarkAborted();
+
+    /**
+     * Aborts sending the item.
+     *
+     * This will give the item an ErrorAttribute of "aborted".
+     * (No need to call setMarkAborted() if you call abort().)
+     */
+    void abort();
+
+private:
+    //@cond PRIVATE
+    class Private;
+    Private *const d;
+
+    Q_PRIVATE_SLOT(d, void doTransport())
+    Q_PRIVATE_SLOT(d, void transportPercent(KJob *, unsigned long))
+    Q_PRIVATE_SLOT(d, void transportResult(KJob *))
+    Q_PRIVATE_SLOT(d, void resourceProgress(const Akonadi::AgentInstance &))
+    Q_PRIVATE_SLOT(d, void resourceResult(qlonglong, int, const QString &))
+    Q_PRIVATE_SLOT(d, void postJobResult(KJob *))
+    Q_PRIVATE_SLOT(d, void doEmitResult(KJob *))
+    Q_PRIVATE_SLOT(d, void slotSentMailCollectionFetched(KJob *))
+    //@endcond
+};
+
+#endif
diff --git a/agents/maildispatcher/sentactionhandler.cpp b/agents/maildispatcher/sentactionhandler.cpp
new file mode 100644 (file)
index 0000000..6073a31
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB,
+        a KDAB Group company, info@kdab.net,
+        author Tobias Koenig <tokoe@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "sentactionhandler.h"
+
+#include <itemfetchjob.h>
+#include <itemmodifyjob.h>
+#include <Akonadi/KMime/MessageFlags>
+#include "maildispatcher_debug.h"
+
+using namespace MailTransport;
+
+SentActionHandler::SentActionHandler(QObject *parent)
+    : QObject(parent)
+{
+}
+
+void SentActionHandler::runAction(const SentActionAttribute::Action &action)
+{
+    if (action.type() == SentActionAttribute::Action::MarkAsReplied ||
+            action.type() == SentActionAttribute::Action::MarkAsForwarded) {
+
+        const Akonadi::Item item(action.value().toLongLong());
+        Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob(item);
+        connect(job, &Akonadi::ItemFetchJob::result, this, &SentActionHandler::itemFetchResult);
+        job->setProperty("type", static_cast<int>(action.type()));
+    }
+}
+
+void SentActionHandler::itemFetchResult(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(MAILDISPATCHER_LOG) << job->errorText();
+        return;
+    }
+
+    Akonadi::ItemFetchJob *fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job);
+    if (fetchJob->items().isEmpty()) {
+        return;
+    }
+
+    Akonadi::Item item = fetchJob->items().at(0);
+
+    const SentActionAttribute::Action::Type type = static_cast<SentActionAttribute::Action::Type>(job->property("type").toInt());
+    if (type == SentActionAttribute::Action::MarkAsReplied) {
+        item.setFlag(Akonadi::MessageFlags::Replied);
+    } else if (type == SentActionAttribute::Action::MarkAsForwarded) {
+        item.setFlag(Akonadi::MessageFlags::Forwarded);
+    }
+
+    Akonadi::ItemModifyJob *modifyJob = new Akonadi::ItemModifyJob(item);
+    modifyJob->setIgnorePayload(true);
+}
+
diff --git a/agents/maildispatcher/sentactionhandler.h b/agents/maildispatcher/sentactionhandler.h
new file mode 100644 (file)
index 0000000..91a1419
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB,
+        a KDAB Group company, info@kdab.net,
+        author Tobias Koenig <tokoe@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef SENTACTIONHANDLER_H
+#define SENTACTIONHANDLER_H
+
+#include <mailtransport/sentactionattribute.h>
+
+#include <QtCore/QObject>
+
+class KJob;
+
+class SentActionHandler : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit SentActionHandler(QObject *parent = Q_NULLPTR);
+
+    void runAction(const MailTransport::SentActionAttribute::Action &action);
+
+private Q_SLOTS:
+    void itemFetchResult(KJob *job);
+};
+
+#endif
diff --git a/agents/maildispatcher/settings.kcfgc b/agents/maildispatcher/settings.kcfgc
new file mode 100644 (file)
index 0000000..8c813dd
--- /dev/null
@@ -0,0 +1,8 @@
+File=maildispatcheragent.kcfg
+ClassName=Settings
+Mutators=true
+ItemAccessors=true
+SetUserTexts=true
+Singleton=true
+#IncludeFiles=
+GlobalEnums=true
diff --git a/agents/maildispatcher/settings.ui b/agents/maildispatcher/settings.ui
new file mode 100644 (file)
index 0000000..e1868bb
--- /dev/null
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <author>Till Adam &lt;adam@kde.org&gt;</author>
+ <class>ConfigDialog</class>
+ <widget class="QWidget" name="ConfigDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>250</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Mail Dispatcher Agent Settings</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>Select the collection to be used as outbox:</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="Akonadi::CollectionRequester" name="outboxSelector">
+     <property name="frameShape">
+      <enum>QFrame::NoFrame</enum>
+     </property>
+     <property name="frameShadow">
+      <enum>QFrame::Plain</enum>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="label_2">
+     <property name="text">
+      <string>Select the collection to move sent messages into:</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="Akonadi::CollectionRequester" name="sentMailSelector">
+     <property name="frameShape">
+      <enum>QFrame::NoFrame</enum>
+     </property>
+     <property name="frameShadow">
+      <enum>QFrame::Plain</enum>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <spacer name="verticalSpacer">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>13</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>Akonadi::CollectionRequester</class>
+   <extends>QFrame</extends>
+   <header>akonadi/collectionrequester.h</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/agents/maildispatcher/storeresultjob.cpp b/agents/maildispatcher/storeresultjob.cpp
new file mode 100644 (file)
index 0000000..6f05681
--- /dev/null
@@ -0,0 +1,141 @@
+/*
+    Copyright (c) 2009 Constantin Berzan <exit3219@gmail.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "storeresultjob.h"
+
+#include <Item>
+#include <ItemFetchJob>
+#include <ItemModifyJob>
+#include <Akonadi/KMime/MessageFlags>
+#include "maildispatcher_debug.h"
+#include <KLocalizedString>
+#include <mailtransport/errorattribute.h>
+#include <mailtransport/dispatchmodeattribute.h>
+
+using namespace Akonadi;
+using namespace MailTransport;
+
+/**
+ * @internal
+ */
+class StoreResultJob::Private
+{
+public:
+    Private(StoreResultJob *qq)
+        : q(qq), success(false)
+    {
+    }
+
+    StoreResultJob *const q;
+    Item item;
+    bool success;
+    QString message;
+
+    // Q_SLOTS:
+    void fetchDone(KJob *job);
+    void modifyDone(KJob *job);
+};
+
+void StoreResultJob::Private::fetchDone(KJob *job)
+{
+    if (job->error()) {
+        return;
+    }
+
+    qCDebug(MAILDISPATCHER_LOG);
+
+    const ItemFetchJob *fetchJob = qobject_cast<ItemFetchJob *>(job);
+    Q_ASSERT(fetchJob);
+    if (fetchJob->items().count() != 1) {
+        qCritical() << "Fetched" << fetchJob->items().count() << "items, expected 1.";
+        q->setError(Unknown);
+        q->setErrorText(i18n("Failed to fetch item."));
+        q->commit();
+        return;
+    }
+
+    // Store result in item.
+    Item item = fetchJob->items().at(0);
+    if (success) {
+        item.clearFlag(Akonadi::MessageFlags::Queued);
+        item.setFlag(Akonadi::MessageFlags::Sent);
+        item.setFlag(Akonadi::MessageFlags::Seen);
+        item.removeAttribute<ErrorAttribute>();
+    } else {
+        item.setFlag(Akonadi::MessageFlags::HasError);
+        ErrorAttribute *errorAttribute = new ErrorAttribute(message);
+        item.addAttribute(errorAttribute);
+
+        // If dispatch failed, set the DispatchModeAttribute to Manual.
+        // Otherwise, the user will *never* be able to try sending the mail again,
+        // as Send Queued Messages will ignore it.
+        if (item.hasAttribute<DispatchModeAttribute>()) {
+            item.attribute<DispatchModeAttribute>()->setDispatchMode(MailTransport::DispatchModeAttribute::Manual);
+        } else {
+            item.addAttribute(new DispatchModeAttribute(MailTransport::DispatchModeAttribute::Manual));
+        }
+    }
+
+    ItemModifyJob *modifyJob = new ItemModifyJob(item, q);
+    QObject::connect(modifyJob, SIGNAL(result(KJob*)), q, SLOT(modifyDone(KJob*)));
+}
+
+void StoreResultJob::Private::modifyDone(KJob *job)
+{
+    if (job->error()) {
+        return;
+    }
+
+    qCDebug(MAILDISPATCHER_LOG);
+
+    q->commit();
+}
+
+StoreResultJob::StoreResultJob(const Item &item, bool success, const QString &message, QObject *parent)
+    : TransactionSequence(parent),
+      d(new Private(this))
+{
+    d->item = item;
+    d->success = success;
+    d->message = message;
+}
+
+StoreResultJob::~StoreResultJob()
+{
+    delete d;
+}
+
+void StoreResultJob::doStart()
+{
+    // Fetch item in case it was modified elsewhere.
+    ItemFetchJob *job = new ItemFetchJob(d->item, this);
+    connect(job, SIGNAL(result(KJob*)), this, SLOT(fetchDone(KJob*)));
+}
+
+bool StoreResultJob::success() const
+{
+    return d->success;
+}
+
+QString StoreResultJob::message() const
+{
+    return d->message;
+}
+
+#include "moc_storeresultjob.cpp"
diff --git a/agents/maildispatcher/storeresultjob.h b/agents/maildispatcher/storeresultjob.h
new file mode 100644 (file)
index 0000000..3426991
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+    Copyright (c) 2009 Constantin Berzan <exit3219@gmail.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef STORERESULTJOB_H
+#define STORERESULTJOB_H
+
+#include <TransactionSequence>
+
+#include <QtCore/QString>
+
+namespace Akonadi
+{
+class Item;
+}
+
+/**
+ * This class stores the result of a StoreResultJob in an item.
+ * First, it removes the 'queued' flag.
+ * After that, if the result was success, it stores the 'sent' flag.
+ * If the result was failure, it stores the 'error' flag and an ErrorAttribute.
+ */
+class StoreResultJob : public Akonadi::TransactionSequence
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new store result job.
+     *
+     * @param item The item to store.
+     * @param success Whether the mail could be dispatched or not.
+     * @param message An error message in case the mail could not be dispatched.
+     * @param parent The parent object.
+     */
+    explicit StoreResultJob(const Akonadi::Item &item, bool success, const QString &message, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Destroys the store result job.
+     */
+    virtual ~StoreResultJob();
+
+    bool success() const;
+    QString message() const;
+
+protected:
+    // reimpl from TransactionSequence
+    void doStart() Q_DECL_OVERRIDE;
+
+private:
+    //@cond PRIVATE
+    class Private;
+    Private *const d;
+
+    Q_PRIVATE_SLOT(d, void fetchDone(KJob *job))
+    Q_PRIVATE_SLOT(d, void modifyDone(KJob *job))
+    //@endcond
+};
+
+#endif
diff --git a/agents/migration/CMakeLists.txt b/agents/migration/CMakeLists.txt
new file mode 100644 (file)
index 0000000..ca14b87
--- /dev/null
@@ -0,0 +1,43 @@
+
+include_directories(
+    ${kdepim-runtime_SOURCE_DIR}/migration
+    ${kdepim-runtime_SOURCE_DIR}
+)
+
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_migration_agent\")
+
+kde_enable_exceptions()
+
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC")
+
+set(migrationagent_SRCS
+    migrationagent.cpp
+    migrationstatuswidget.cpp
+    migrationexecutor.cpp
+    migrationscheduler.cpp
+    autotests/dummymigrator.cpp
+)
+
+add_executable(akonadi_migration_agent ${migrationagent_SRCS})
+
+if( APPLE )
+  set_target_properties(akonadi_migration_agent PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../Info.plist.template)
+  set_target_properties(akonadi_migration_agent PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.migrationagent")
+  set_target_properties(akonadi_migration_agent PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi Migrationagent")
+endif ()
+
+target_link_libraries(akonadi_migration_agent
+    gidmigration
+    KF5::AkonadiCore
+    KF5::AkonadiAgentBase
+    KF5::Contacts
+    KF5::WindowSystem
+    KF5::JobWidgets
+)
+
+install(TARGETS akonadi_migration_agent ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
+install(FILES migrationagent.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}//akonadi/agents")
+
+if(BUILD_TESTING)
+    add_subdirectory(autotests)
+endif()
diff --git a/agents/migration/Messages.sh b/agents/migration/Messages.sh
new file mode 100755 (executable)
index 0000000..dd6f739
--- /dev/null
@@ -0,0 +1,2 @@
+#! /bin/sh
+$XGETTEXT *.cpp -o $podir/akonadi_migration_agent.pot
diff --git a/agents/migration/autotests/CMakeLists.txt b/agents/migration/autotests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..7bda437
--- /dev/null
@@ -0,0 +1,12 @@
+
+include_directories(
+    ${CMAKE_CURRENT_SOURCE_DIR}/..
+)
+
+add_executable(schedulertest schedulertest.cpp ../migrationscheduler.cpp ../migrationexecutor.cpp)
+target_link_libraries(schedulertest
+    gidmigration
+    KF5::AkonadiCore
+    Qt5::Test
+)
+add_test(schedulertest schedulertest)
diff --git a/agents/migration/autotests/dummymigrator.cpp b/agents/migration/autotests/dummymigrator.cpp
new file mode 100644 (file)
index 0000000..1d4a1f0
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2013  Christian Mollekopf <mollekopf@kolabsys.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License or (at your option) version 3 or any later version
+ * accepted by the membership of KDE e.V. (or its successor approved
+ * by the membership of KDE e.V.), which shall act as a proxy
+ * defined in Section 14 of version 3 of the license.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "dummymigrator.h"
+#include <QTimer>
+#include <QDebug>
+
+DummyMigrator::DummyMigrator(const QString &identifier)
+    :   MigratorBase(QLatin1String("dummymigrator") + identifier, QString(), QString())
+{}
+
+QString DummyMigrator::displayName() const
+{
+    return QStringLiteral("dummymigrator");
+}
+
+void DummyMigrator::startWork()
+{
+    qDebug();
+    QTimer::singleShot(10000, this, &DummyMigrator::onTimerElapsed);
+}
+
+void DummyMigrator::onTimerElapsed()
+{
+    qDebug();
+    setMigrationState(Complete);
+}
+
+bool DummyMigrator::shouldAutostart() const
+{
+    return true;
+}
+
+bool DummyMigrator::canStart()
+{
+    return true;
+}
+
+void DummyMigrator::pause()
+{
+    qDebug();
+    MigratorBase::pause();
+}
+
+void DummyMigrator::abort()
+{
+    qDebug();
+    MigratorBase::abort();
+}
+
diff --git a/agents/migration/autotests/dummymigrator.h b/agents/migration/autotests/dummymigrator.h
new file mode 100644 (file)
index 0000000..30a7ec7
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2013  Christian Mollekopf <mollekopf@kolabsys.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License or (at your option) version 3 or any later version
+ * accepted by the membership of KDE e.V. (or its successor approved
+ * by the membership of KDE e.V.), which shall act as a proxy
+ * defined in Section 14 of version 3 of the license.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef DUMMYMIGRATOR_H
+#define DUMMYMIGRATOR_H
+
+#include <migration/migratorbase.h>
+
+/**
+ * Dummy migrator that simply completes after 10s and always autostarts.
+ * Add to the scheduler to play with the migrationagent.
+ */
+class DummyMigrator : public MigratorBase
+{
+    Q_OBJECT
+public:
+    explicit DummyMigrator(const QString &identifier);
+
+    QString displayName() const Q_DECL_OVERRIDE;
+    void startWork() Q_DECL_OVERRIDE;
+
+    bool shouldAutostart() const Q_DECL_OVERRIDE;
+    bool canStart() Q_DECL_OVERRIDE;
+    void pause() Q_DECL_OVERRIDE;
+
+    void abort() Q_DECL_OVERRIDE;
+private Q_SLOTS:
+    void onTimerElapsed();
+};
+
+#endif
\ No newline at end of file
diff --git a/agents/migration/autotests/schedulertest.cpp b/agents/migration/autotests/schedulertest.cpp
new file mode 100644 (file)
index 0000000..11d849c
--- /dev/null
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2013  Christian Mollekopf <mollekopf@kolabsys.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License or (at your option) version 3 or any later version
+ * accepted by the membership of KDE e.V. (or its successor approved
+ * by the membership of KDE e.V.), which shall act as a proxy
+ * defined in Section 14 of version 3 of the license.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <QTest>
+#include <QSignalSpy>
+#include <QDebug>
+#include <KJobTrackerInterface>
+
+#include "../migrationscheduler.h"
+#include <migration/migratorbase.h>
+
+Q_DECLARE_METATYPE(QModelIndex)
+
+class Testmigrator: public MigratorBase
+{
+    Q_OBJECT
+public:
+    explicit Testmigrator(const QString &identifier, QObject *parent = Q_NULLPTR):
+        MigratorBase(QLatin1String("testmigrator") + identifier, QString(), QString(), parent), mAutostart(false)
+    {}
+
+    QString displayName() const Q_DECL_OVERRIDE
+    {
+        return QStringLiteral("name");
+    }
+
+    void startWork() Q_DECL_OVERRIDE
+    {}
+
+    void abort() Q_DECL_OVERRIDE {
+        setMigrationState(Aborted);
+    }
+
+    virtual void complete()
+    {
+        setMigrationState(Complete);
+    }
+
+    bool shouldAutostart() const Q_DECL_OVERRIDE
+    {
+        return mAutostart;
+    }
+
+    void pause() Q_DECL_OVERRIDE {
+        setMigrationState(Paused);
+    }
+
+    void resume() Q_DECL_OVERRIDE {
+        setMigrationState(InProgress);
+    }
+
+    bool mAutostart;
+};
+
+class TestJobTracker : public KJobTrackerInterface
+{
+public:
+    TestJobTracker() : mPercent(0)
+    {}
+
+    void registerJob(KJob *job) Q_DECL_OVERRIDE {
+        KJobTrackerInterface::registerJob(job);
+        mJobs << job;
+    }
+
+    void unregisterJob(KJob *job) Q_DECL_OVERRIDE {
+        mJobs.removeAll(job);
+    }
+
+    void finished(KJob *job) Q_DECL_OVERRIDE {
+        mJobs.removeAll(job);
+    }
+
+    void percent(KJob *job, long unsigned int percent) Q_DECL_OVERRIDE {
+        Q_UNUSED(job);
+        mPercent = percent;
+    }
+
+    QList<KJob *> mJobs;
+    int mPercent;
+};
+
+class SchedulerTest: public QObject
+{
+    Q_OBJECT
+private Q_SLOTS:
+
+    void initTestcase()
+    {
+        qRegisterMetaType<QModelIndex>();
+    }
+
+    void testInsertRow()
+    {
+        MigrationScheduler scheduler;
+        QAbstractItemModel &model(scheduler.model());
+
+        QCOMPARE(model.rowCount(), 0);
+
+        QSignalSpy rowsInsertedSpy(&model, SIGNAL(rowsInserted(QModelIndex,int,int)));
+        QVERIFY(rowsInsertedSpy.isValid());
+
+        scheduler.addMigrator(QSharedPointer<Testmigrator>(new Testmigrator(QStringLiteral("id"))));
+        QCOMPARE(model.rowCount(), 1);
+        QCOMPARE(rowsInsertedSpy.count(), 1);
+
+        QVERIFY(model.index(0, 0).isValid());
+        QVERIFY(!model.index(1, 0).isValid());
+
+        scheduler.addMigrator(QSharedPointer<Testmigrator>(new Testmigrator(QStringLiteral("id2"))));
+        QCOMPARE(model.rowCount(), 2);
+        QCOMPARE(rowsInsertedSpy.count(), 2);
+    }
+
+    void testDisplayName()
+    {
+        MigrationScheduler scheduler;
+        scheduler.addMigrator(QSharedPointer<Testmigrator>(new Testmigrator(QStringLiteral("id"))));
+        QAbstractItemModel &model(scheduler.model());
+        QCOMPARE(model.data(model.index(0, 0)).toString(), QStringLiteral("name"));
+    }
+
+    void testStartStop()
+    {
+        MigrationScheduler scheduler;
+        QSharedPointer<Testmigrator> migrator(new Testmigrator(QStringLiteral("id")));
+        scheduler.addMigrator(migrator);
+
+        scheduler.start(migrator->identifier());
+        QCOMPARE(migrator->migrationState(), MigratorBase::InProgress);
+
+        scheduler.abort(migrator->identifier());
+        QCOMPARE(migrator->migrationState(), MigratorBase::Aborted);
+    }
+
+    void testNoDuplicates()
+    {
+        MigrationScheduler scheduler;
+        scheduler.addMigrator(QSharedPointer<Testmigrator>(new Testmigrator(QStringLiteral("id"))));
+        scheduler.addMigrator(QSharedPointer<Testmigrator>(new Testmigrator(QStringLiteral("id"))));
+        QAbstractItemModel &model(scheduler.model());
+        QCOMPARE(model.rowCount(), 1);
+    }
+
+    void testMigrationStateChanged()
+    {
+        MigrationScheduler scheduler;
+        scheduler.addMigrator(QSharedPointer<Testmigrator>(new Testmigrator(QStringLiteral("id1"))));
+        QSharedPointer<Testmigrator> migrator(new Testmigrator(QStringLiteral("id2")));
+        scheduler.addMigrator(migrator);
+        scheduler.addMigrator(QSharedPointer<Testmigrator>(new Testmigrator(QStringLiteral("id3"))));
+        QAbstractItemModel &model(scheduler.model());
+
+        QSignalSpy spy(&model, SIGNAL(dataChanged(QModelIndex,QModelIndex)));
+        QVERIFY(spy.isValid());
+        migrator->start();
+
+        QCOMPARE(spy.count(), 1);
+        const QVariantList args = spy.takeFirst();
+        QCOMPARE(args.at(0).value<QModelIndex>().row(), 1);
+        QCOMPARE(args.at(1).value<QModelIndex>().row(), 1);
+    }
+
+    void testRunMultiple()
+    {
+        MigrationScheduler scheduler;
+
+        QSharedPointer<Testmigrator> m1(new Testmigrator(QStringLiteral("id1")));
+        scheduler.addMigrator(m1);
+
+        QSharedPointer<Testmigrator> m2(new Testmigrator(QStringLiteral("id2")));
+        scheduler.addMigrator(m2);
+
+        scheduler.start(m1->identifier());
+        scheduler.start(m2->identifier());
+
+        QCOMPARE(m1->migrationState(), MigratorBase::InProgress);
+        QCOMPARE(m2->migrationState(), MigratorBase::InProgress);
+    }
+
+    void testRunAutostart()
+    {
+        MigrationScheduler scheduler;
+
+        QSharedPointer<Testmigrator> m1(new Testmigrator(QStringLiteral("id1")));
+        m1->mAutostart = true;
+        scheduler.addMigrator(m1);
+
+        QSharedPointer<Testmigrator> m2(new Testmigrator(QStringLiteral("id2")));
+        m2->mAutostart = true;
+        scheduler.addMigrator(m2);
+
+        QCOMPARE(m1->migrationState(), MigratorBase::InProgress);
+        qDebug() << m2->migrationState();
+        QCOMPARE(m2->migrationState(), MigratorBase::None);
+        m1->complete();
+        QCOMPARE(m2->migrationState(), MigratorBase::InProgress);
+
+    }
+
+    void testJobTracker()
+    {
+        TestJobTracker jobTracker;
+        MigrationScheduler scheduler(&jobTracker);
+        QSharedPointer<Testmigrator> m1(new Testmigrator(QStringLiteral("id1")));
+        m1->mAutostart = true;
+        scheduler.addMigrator(m1);
+
+        QCOMPARE(jobTracker.mJobs.size(), 1);
+
+        m1->complete();
+
+        //Give the job some time to delete itself
+        QTest::qWait(500);
+
+        QCOMPARE(jobTracker.mJobs.size(), 0);
+    }
+
+    void testSuspend()
+    {
+        TestJobTracker jobTracker;
+        MigrationScheduler scheduler(&jobTracker);
+        QSharedPointer<Testmigrator> m1(new Testmigrator(QStringLiteral("id1")));
+        m1->mAutostart = true;
+        scheduler.addMigrator(m1);
+        jobTracker.mJobs.first()->suspend();
+        QCOMPARE(m1->migrationState(), MigratorBase::Paused);
+        jobTracker.mJobs.first()->resume();
+        QCOMPARE(m1->migrationState(), MigratorBase::InProgress);
+    }
+
+    /*
+     * Even if the migrator doesn't implement suspend, the executor suspends after completing the current job and waits with starting the second job.
+     */
+    void testJobFinishesDuringSuspend()
+    {
+        TestJobTracker jobTracker;
+        MigrationScheduler scheduler(&jobTracker);
+        QSharedPointer<Testmigrator> m1(new Testmigrator(QStringLiteral("id1")));
+        m1->mAutostart = true;
+        scheduler.addMigrator(m1);
+        QSharedPointer<Testmigrator> m2(new Testmigrator(QStringLiteral("id2")));
+        m2->mAutostart = true;
+        scheduler.addMigrator(m2);
+        jobTracker.mJobs.first()->suspend();
+        m1->complete();
+        QCOMPARE(m2->migrationState(), MigratorBase::None);
+        jobTracker.mJobs.first()->resume();
+        QCOMPARE(m2->migrationState(), MigratorBase::InProgress);
+    }
+
+    void testProgressReporting()
+    {
+        TestJobTracker jobTracker;
+        MigrationScheduler scheduler(&jobTracker);
+        QSharedPointer<Testmigrator> m1(new Testmigrator(QStringLiteral("id1")));
+        m1->mAutostart = true;
+        scheduler.addMigrator(m1);
+        QCOMPARE(jobTracker.mPercent, 0);
+        m1->complete();
+        QCOMPARE(jobTracker.mPercent, 100);
+    }
+
+};
+
+QTEST_MAIN(SchedulerTest)
+
+#include "schedulertest.moc"
diff --git a/agents/migration/migrationagent.cpp b/agents/migration/migrationagent.cpp
new file mode 100644 (file)
index 0000000..71e9af1
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2013  Christian Mollekopf <mollekopf@kolabsys.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License or (at your option) version 3 or any later version
+ * accepted by the membership of KDE e.V. (or its successor approved
+ * by the membership of KDE e.V.), which shall act as a proxy
+ * defined in Section 14 of version 3 of the license.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#include "migrationagent.h"
+
+#include "migrationstatuswidget.h"
+
+#include <migration/gid/gidmigrator.h>
+#include <KContacts/Addressee>
+#include <KWindowSystem>
+#include <QDialog>
+#include <KLocalizedString>
+#include <kuiserverjobtracker.h>
+#include <QDialogButtonBox>
+#include <QVBoxLayout>
+
+namespace Akonadi
+{
+
+MigrationAgent::MigrationAgent(const QString &id)
+    :   AgentBase(id),
+        mScheduler(new KUiServerJobTracker)
+{
+    KLocalizedString::setApplicationDomain("akonadi_migration_agent");
+    mScheduler.addMigrator(QSharedPointer<GidMigrator>(new GidMigrator(KContacts::Addressee::mimeType())));
+}
+
+void MigrationAgent::configure(WId windowId)
+{
+    QDialog *dlg = new QDialog();
+    QVBoxLayout *topLayout = new QVBoxLayout;
+    dlg->setLayout(topLayout);
+
+    MigrationStatusWidget *widget = new MigrationStatusWidget(mScheduler, dlg);
+    topLayout->addWidget(widget);
+    dlg->setAttribute(Qt::WA_DeleteOnClose);
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close);
+    connect(buttonBox, &QDialogButtonBox::accepted, dlg, &QDialog::accept);
+    connect(buttonBox, &QDialogButtonBox::rejected, dlg, &QDialog::reject);
+    topLayout->addWidget(buttonBox);
+
+    dlg->setWindowTitle(i18nc("Title of the window that shows the status of the migration agent and offers controls to start/stop individual migration jobs.", "Migration Status"));
+    dlg->resize(600, 300);
+
+    if (windowId) {
+#ifndef Q_OS_WIN
+        KWindowSystem::setMainWindow(dlg, windowId);
+#else
+        KWindowSystem::setMainWindow(dlg, (HWND)windowId);
+#endif
+    }
+    dlg->show();
+}
+
+}
+
+AKONADI_AGENT_MAIN(Akonadi::MigrationAgent)
+
diff --git a/agents/migration/migrationagent.desktop b/agents/migration/migrationagent.desktop
new file mode 100644 (file)
index 0000000..299271c
--- /dev/null
@@ -0,0 +1,45 @@
+[Desktop Entry]
+Name=Migration Agent
+Name[bs]=Migracijski agent
+Name[ca]=Agent de migració
+Name[ca@valencia]=Agent de migració
+Name[da]=Migreringsagent
+Name[de]=Migrations-Assistent
+Name[el]=Πράκτορας μεταφοράς
+Name[en_GB]=Migration Agent
+Name[es]=Agente de migración
+Name[et]=Migreerimisagent
+Name[fi]=Siirtoagentti
+Name[fr]=Agent de migration
+Name[gl]=Axente de migración
+Name[hu]=Költöztető ügynök
+Name[ia]=Agente de migration
+Name[it]=Agente di migrazione
+Name[kk]=Көшіп ауысу агенті
+Name[ko]=이전 마법사
+Name[lt]=Migravimo agentas
+Name[nb]=Migreringsagent
+Name[nds]=Ümwannelhölper
+Name[nl]=Migratie-agent
+Name[pl]=Agent migracji
+Name[pt]=Agente de Migração
+Name[pt_BR]=Agente de Migração
+Name[ru]=Агент переноса данных
+Name[sk]=Agent migrácie
+Name[sl]=Posrednik za selitev
+Name[sr]=Агент селидбе
+Name[sr@ijekavian]=Агент селидбе
+Name[sr@ijekavianlatin]=Agent selidbe
+Name[sr@latin]=Agent selidbe
+Name[sv]=Överföringsmodul
+Name[tr]=Taşıma Yardımcısı
+Name[uk]=Агент перенесення
+Name[x-test]=xxMigration Agentxx
+Name[zh_CN]=迁移助手
+Name[zh_TW]=移植代理程式
+Type=AkonadiAgent
+Exec=akonadi_migration_agent
+Icon=system-reboot
+
+X-Akonadi-Capabilities=Unique,Autostart
+X-Akonadi-Identifier=akonadi_migration_agent
diff --git a/agents/migration/migrationagent.h b/agents/migration/migrationagent.h
new file mode 100644 (file)
index 0000000..690cdab
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2013  Christian Mollekopf <mollekopf@kolabsys.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License or (at your option) version 3 or any later version
+ * accepted by the membership of KDE e.V. (or its successor approved
+ * by the membership of KDE e.V.), which shall act as a proxy
+ * defined in Section 14 of version 3 of the license.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MIGRATIONAGENT_H
+#define MIGRATIONAGENT_H
+
+#include <agentbase.h>
+#include "migrationscheduler.h"
+
+namespace Akonadi
+{
+
+class MigrationAgent : public AgentBase, public AgentBase::ObserverV2
+{
+    Q_OBJECT
+public:
+    explicit MigrationAgent(const QString &id);
+    void configure(WId windowId) Q_DECL_OVERRIDE;
+private:
+    MigrationScheduler mScheduler;
+};
+
+}
+
+#endif
diff --git a/agents/migration/migrationexecutor.cpp b/agents/migration/migrationexecutor.cpp
new file mode 100644 (file)
index 0000000..fa2c532
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2013  Christian Mollekopf <mollekopf@kolabsys.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License or (at your option) version 3 or any later version
+ * accepted by the membership of KDE e.V. (or its successor approved
+ * by the membership of KDE e.V.), which shall act as a proxy
+ * defined in Section 14 of version 3 of the license.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#include "migrationexecutor.h"
+
+#include <KLocalizedString>
+
+MigrationExecutor::MigrationExecutor()
+    :   KJob(),
+        mSuspended(false),
+        mTotalAmount(0),
+        mAlreadyProcessed(0)
+{
+    setCapabilities(Suspendable);
+}
+
+void MigrationExecutor::start()
+{
+    setPercent(0);
+    Q_EMIT description(this, i18nc("User visible name of ongoing Akonadi migration jobs", "PIM Maintenance"));
+}
+
+void MigrationExecutor::add(const QSharedPointer<MigratorBase> &migrator)
+{
+    mTotalAmount++;
+    mQueue.enqueue(migrator.toWeakRef());
+    executeNext();
+}
+
+void MigrationExecutor::executeNext()
+{
+    if (mCurrentMigrator || mSuspended) {
+        return;
+    }
+    QSharedPointer<MigratorBase> migrator;
+    while (!migrator && !mQueue.isEmpty()) {
+        mCurrentMigrator = mQueue.dequeue();
+        migrator = mCurrentMigrator.toStrongRef();
+    }
+    if (migrator) {
+        Q_EMIT infoMessage(this, i18nc("PIM-Maintenance is in progress.", "In progress..."));
+        connect(migrator.data(), &MigratorBase::stoppedProcessing, this, &MigrationExecutor::onStoppedProcessing);
+        migrator->start();
+    } else {
+        // Reset the notification status, otherwise we get notification "In progress...[finished]"
+        // without any description that it's related to PIM-Maintenance
+        Q_EMIT infoMessage(this, i18n("PIM Maintenance"));
+        emitResult();
+    }
+}
+
+void MigrationExecutor::onStoppedProcessing()
+{
+    mAlreadyProcessed++;
+    Q_ASSERT(mTotalAmount > 0);
+    //TODO: setProcessedAmount would be better, but we need support for suitable units first (there's only files, folders, bytes).
+    setPercent(mAlreadyProcessed * 100.0 / mTotalAmount);
+    mCurrentMigrator.clear();
+    executeNext();
+}
+
+bool MigrationExecutor::doSuspend()
+{
+    if (mCurrentMigrator) {
+        QSharedPointer<MigratorBase> migrator = mCurrentMigrator.toStrongRef();
+        if (migrator) {
+            migrator->pause();
+        } else {
+            mCurrentMigrator.clear();
+        }
+    }
+    Q_EMIT infoMessage(this, i18nc("PIM-Maintenance is paused.", "Paused."));
+    mSuspended = true;
+    return true;
+}
+
+bool MigrationExecutor::doResume()
+{
+    mSuspended = false;
+    if (mCurrentMigrator) {
+        QSharedPointer<MigratorBase> migrator = mCurrentMigrator.toStrongRef();
+        if (migrator) {
+            migrator->resume();
+        } else {
+            mCurrentMigrator.clear();
+        }
+    }
+    executeNext();
+    return true;
+}
+
diff --git a/agents/migration/migrationexecutor.h b/agents/migration/migrationexecutor.h
new file mode 100644 (file)
index 0000000..2e50e89
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2013  Christian Mollekopf <mollekopf@kolabsys.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License or (at your option) version 3 or any later version
+ * accepted by the membership of KDE e.V. (or its successor approved
+ * by the membership of KDE e.V.), which shall act as a proxy
+ * defined in Section 14 of version 3 of the license.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MIGRATIONEXECUTOR_H
+#define MIGRATIONEXECUTOR_H
+
+#include <KJob>
+#include <QQueue>
+#include <QSharedPointer>
+#include <migration/migratorbase.h>
+
+/**
+ * An executor can contain multiple jobs that are scheduled by the executor.
+ *
+ * The executor is responsible for starting/pausing/stopping the individual migrators.
+ *
+ * This job is used to give overall progress information and start/stop controls to KUIServer via KUiServerJobTracker.
+ */
+class MigrationExecutor : public KJob
+{
+    Q_OBJECT
+public:
+    MigrationExecutor();
+    void add(const QSharedPointer<MigratorBase> &);
+    void start() Q_DECL_OVERRIDE;
+
+protected:
+    bool doResume() Q_DECL_OVERRIDE;
+    bool doSuspend() Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void onStoppedProcessing();
+    void executeNext();
+
+private:
+    QQueue< QWeakPointer<MigratorBase> > mQueue;
+    QWeakPointer<MigratorBase> mCurrentMigrator;
+    bool mSuspended;
+    int mTotalAmount;
+    int mAlreadyProcessed;
+};
+
+#endif
diff --git a/agents/migration/migrationscheduler.cpp b/agents/migration/migrationscheduler.cpp
new file mode 100644 (file)
index 0000000..cf12c17
--- /dev/null
@@ -0,0 +1,298 @@
+/*
+ * Copyright 2013  Christian Mollekopf <mollekopf@kolabsys.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License or (at your option) version 3 or any later version
+ * accepted by the membership of KDE e.V. (or its successor approved
+ * by the membership of KDE e.V.), which shall act as a proxy
+ * defined in Section 14 of version 3 of the license.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "migrationscheduler.h"
+
+#include <KLocalizedString>
+#include <QDebug>
+#include <QIcon>
+#include <KJobTrackerInterface>
+
+#include "migrationexecutor.h"
+
+void LogModel::message(MigratorBase::MessageType type, const QString &msg)
+{
+    switch (type) {
+    case MigratorBase::Success: {
+        QStandardItem *item = new QStandardItem(QIcon::fromTheme(QStringLiteral("dialog-ok-apply")), msg);
+        item->setEditable(false);
+        appendRow(item);
+        break;
+    }
+    case MigratorBase::Skip: {
+        QStandardItem *item = new QStandardItem(QIcon::fromTheme(QStringLiteral("dialog-ok")), msg);
+        item->setEditable(false);
+        appendRow(item);
+        break;
+    }
+    case MigratorBase::Info: {
+        QStandardItem *item = new QStandardItem(QIcon::fromTheme(QStringLiteral("dialog-information")), msg);
+        item->setEditable(false);
+        appendRow(item);
+        break;
+    }
+    case MigratorBase::Warning: {
+        QStandardItem *item = new QStandardItem(QIcon::fromTheme(QStringLiteral("dialog-warning")), msg);
+        item->setEditable(false);
+        appendRow(item);
+        break;
+    }
+    case MigratorBase::Error: {
+        QStandardItem *item = new QStandardItem(QIcon::fromTheme(QStringLiteral("dialog-error")), msg);
+        item->setEditable(false);
+        appendRow(item);
+        break;
+    }
+    default:
+        qCritical() << "unknown type " << type;
+    }
+}
+
+Row::Row(const QSharedPointer<MigratorBase> &migrator, MigratorModel &model)
+    :   QObject(),
+        mMigrator(migrator),
+        mModel(model)
+{
+    connect(migrator.data(), &MigratorBase::stateChanged, this, &Row::stateChanged);
+    connect(migrator.data(), SIGNAL(progress(int)), this, SLOT(progress(int)));
+}
+
+bool Row::operator==(const Row &other) const
+{
+    return mMigrator->identifier() == other.mMigrator->identifier();
+}
+
+void Row::stateChanged(MigratorBase::MigrationState /*newState*/)
+{
+    mModel.columnChanged(*this, MigratorModel::State);
+}
+
+void Row::progress(int /*prog*/)
+{
+    mModel.columnChanged(*this, MigratorModel::Progress);
+}
+
+int MigratorModel::positionOf(const Row &row)
+{
+    int pos = 0;
+    foreach (const QSharedPointer<Row> &r, mMigrators) {
+        if (row == *r) {
+            return pos;
+        }
+        pos++;
+    }
+    return -1;
+}
+
+void MigratorModel::columnChanged(const Row &row, int col)
+{
+    const int p = positionOf(row);
+    Q_ASSERT(p >= 0);
+    if (p >= 0) {
+        const QModelIndex idx = index(p, col);
+        Q_EMIT dataChanged(idx, idx);
+    }
+}
+
+bool MigratorModel::addMigrator(const QSharedPointer<MigratorBase> &m)
+{
+    if (migrator(m->identifier())) {
+        qWarning() << "Model already contains a migrator with the identifier: " << m;
+        return false;
+    }
+    const int pos = mMigrators.size();
+    beginInsertRows(QModelIndex(), pos, pos);
+    mMigrators.append(QSharedPointer<Row>(new Row(m, *this)));
+    endInsertRows();
+    return true;
+}
+
+int MigratorModel::columnCount(const QModelIndex &/*parent*/) const
+{
+    return ColumnCount;
+}
+
+int MigratorModel::rowCount(const QModelIndex &parent) const
+{
+    if (!parent.isValid()) {
+        return mMigrators.size();
+    }
+    return 0;
+}
+
+QModelIndex MigratorModel::index(int row, int column, const QModelIndex &parent) const
+{
+    if (row >= rowCount(parent) || row < 0) {
+        return QModelIndex();
+    }
+    return createIndex(row, column, static_cast<void *>(mMigrators.at(row).data()));
+}
+
+QModelIndex MigratorModel::parent(const QModelIndex &/*child*/) const
+{
+    return QModelIndex();
+}
+
+QVariant MigratorModel::headerData(int section, Qt::Orientation /*orientation*/, int role) const
+{
+    if (role == Qt::DisplayRole) {
+        switch (section) {
+        case Name:
+            return i18nc("Name of the migrator in this row", "Name");
+        case Progress:
+            return i18nc("Progress of the mgirator in %", "Progress");
+        case State:
+            return i18nc("Current status of the migrator (done, in progress, ...)", "Status");
+        default:
+            Q_ASSERT(false);
+        }
+    }
+    return QVariant();
+}
+
+QVariant MigratorModel::data(const QModelIndex &index, int role) const
+{
+    const Row *row = static_cast<Row *>(index.internalPointer());
+    const QSharedPointer<MigratorBase> migrator(row->mMigrator);
+    if (!migrator) {
+        qWarning() << "migrator not found";
+        return QVariant();
+    }
+    switch (role) {
+    case Qt::DisplayRole:
+        switch (index.column()) {
+        case Name:
+            return migrator->displayName();
+        case Progress:
+            return QStringLiteral("%1 %").arg(migrator->progress());
+        case State:
+            return migrator->status();
+        default:
+            Q_ASSERT(false);
+        }
+    case IdentifierRole:
+        return migrator->identifier();
+    case LogfileRole:
+        return migrator->logfile();
+    case Qt::ToolTipRole:
+        return migrator->description();
+    default:
+        break;
+    }
+    return QVariant();
+}
+
+QSharedPointer<MigratorBase> MigratorModel::migrator(const QString &identifier) const
+{
+    foreach (const QSharedPointer<Row> &row, mMigrators) {
+        if (row->mMigrator->identifier() == identifier) {
+            return row->mMigrator;
+        }
+    }
+    return QSharedPointer<MigratorBase>();
+}
+
+QList< QSharedPointer<MigratorBase> > MigratorModel::migrators() const
+{
+    QList< QSharedPointer<MigratorBase> > migrators;
+    foreach (const QSharedPointer<Row> &row, mMigrators) {
+        return migrators << row->mMigrator;
+    }
+    return migrators;
+}
+
+MigrationScheduler::MigrationScheduler(KJobTrackerInterface *jobTracker, QObject *parent)
+    : QObject(parent),
+      mModel(new MigratorModel),
+      mJobTracker(jobTracker)
+{
+}
+
+MigrationScheduler::~MigrationScheduler()
+{
+    delete mAutostartExecutor;
+}
+
+void MigrationScheduler::addMigrator(const QSharedPointer<MigratorBase> &migrator)
+{
+    if (mModel->addMigrator(migrator)) {
+        QSharedPointer<LogModel> logModel(new LogModel);
+        connect(migrator.data(), &MigratorBase::message, logModel.data(), &LogModel::message);
+        mLogModel.insert(migrator->identifier(), logModel);
+        if (migrator->shouldAutostart()) {
+            checkForAutostart(migrator);
+        }
+    }
+}
+
+QAbstractItemModel &MigrationScheduler::model()
+{
+    return *mModel;
+}
+
+QStandardItemModel &MigrationScheduler::logModel(const QString &identifier)
+{
+    Q_ASSERT(mLogModel.contains(identifier));
+    return *mLogModel.value(identifier);
+}
+
+void MigrationScheduler::checkForAutostart(const QSharedPointer<MigratorBase> &migrator)
+{
+    if (migrator->migrationState() != MigratorBase::Complete) {
+
+        if (!mAutostartExecutor) {
+            mAutostartExecutor = new MigrationExecutor;
+            if (mJobTracker) {
+                mJobTracker->registerJob(mAutostartExecutor);
+            }
+
+            mAutostartExecutor->start();
+        }
+
+        mAutostartExecutor->add(migrator);
+    }
+}
+
+void MigrationScheduler::start(const QString &identifier)
+{
+    //TODO create separate executor?
+    const QSharedPointer<MigratorBase> m = mModel->migrator(identifier);
+    if (m) {
+        m->start();
+    }
+}
+
+void MigrationScheduler::pause(const QString &identifier)
+{
+    const QSharedPointer<MigratorBase> m = mModel->migrator(identifier);
+    if (m) {
+        m->pause();
+    }
+}
+
+void MigrationScheduler::abort(const QString &identifier)
+{
+    const QSharedPointer<MigratorBase> m = mModel->migrator(identifier);
+    if (m) {
+        m->abort();
+    }
+}
+
diff --git a/agents/migration/migrationscheduler.h b/agents/migration/migrationscheduler.h
new file mode 100644 (file)
index 0000000..8823b8a
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2013  Christian Mollekopf <mollekopf@kolabsys.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License or (at your option) version 3 or any later version
+ * accepted by the membership of KDE e.V. (or its successor approved
+ * by the membership of KDE e.V.), which shall act as a proxy
+ * defined in Section 14 of version 3 of the license.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MIGRATIONSCHEDULER_H
+#define MIGRATIONSCHEDULER_H
+
+#include "migratorbase.h"
+#include <QObject>
+#include <QAbstractItemModel>
+#include <QSharedPointer>
+#include <QPointer>
+#include <QStandardItemModel>
+
+class MigrationExecutor;
+class KJobTrackerInterface;
+class MigratorModel;
+
+class LogModel : public QStandardItemModel
+{
+    Q_OBJECT
+public Q_SLOTS:
+    void message(MigratorBase::MessageType type, const QString &msg);
+};
+
+class Row: public QObject
+{
+    Q_OBJECT
+public:
+    QSharedPointer<MigratorBase> mMigrator;
+    MigratorModel &mModel;
+
+    explicit Row(const QSharedPointer<MigratorBase> &migrator, MigratorModel &model);
+
+    bool operator==(const Row &other) const;
+
+private Q_SLOTS:
+    void stateChanged(MigratorBase::MigrationState);
+    void progress(int);
+};
+
+/**
+ * The model serves as container for the migrators and exposes the status of each migrator.
+ *
+ * It can be plugged into a Listview to inform about the migration progress.
+ */
+class MigratorModel: public QAbstractItemModel
+{
+public:
+    enum Roles {
+        IdentifierRole = Qt::UserRole + 1,
+        LogfileRole
+    };
+    bool addMigrator(const QSharedPointer<MigratorBase> &migrator);
+
+    int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
+    int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
+    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE;
+    QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
+    QModelIndex parent(const QModelIndex &child) const Q_DECL_OVERRIDE;
+    QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE;
+
+    QSharedPointer<MigratorBase> migrator(const QString &identifier) const;
+    QList< QSharedPointer<MigratorBase> > migrators() const;
+
+private:
+    enum Columns {
+        Name = 0,
+        Progress = 1,
+        State = 2,
+        ColumnCount
+    };
+    friend class Row;
+    int positionOf(const Row &);
+    void columnChanged(const Row &, int column);
+    QList< QSharedPointer<Row> > mMigrators;
+};
+
+/**
+ * Scheduler for migration jobs.
+ *
+ * Status information is exposed via getModel, which returns a list model containing all migrators with basic information.
+ * Additionally a logmodel is available via getLogModel for each migrator. The logmodel is continuously filled with information, and can be requested and displayed at any time.
+ *
+ * Migrators which return true on shouldAutostart() automatically enter a queue to be processed one after the other.
+ * When manually triggered it is possible though to run multiple jobs in parallel.
+ */
+class MigrationScheduler : public QObject
+{
+    Q_OBJECT
+public:
+    explicit MigrationScheduler(KJobTrackerInterface *jobTracker = Q_NULLPTR, QObject *parent = Q_NULLPTR);
+    virtual ~MigrationScheduler();
+
+    void addMigrator(const QSharedPointer<MigratorBase> &migrator);
+
+    //A model for the view
+    QAbstractItemModel &model();
+    QStandardItemModel &logModel(const QString &identifier);
+
+    //Control
+    void start(const QString &identifier);
+    void pause(const QString &identifier);
+    void abort(const QString &identifier);
+
+private:
+    void checkForAutostart(const QSharedPointer<MigratorBase> &migrator);
+
+    QScopedPointer<MigratorModel> mModel;
+    QHash<QString, QSharedPointer<LogModel> > mLogModel;
+    QPointer<MigrationExecutor> mAutostartExecutor;
+    KJobTrackerInterface *mJobTracker;
+};
+
+#endif // MIGRATIONSCHEDULER_H
diff --git a/agents/migration/migrationstatuswidget.cpp b/agents/migration/migrationstatuswidget.cpp
new file mode 100644 (file)
index 0000000..6264e00
--- /dev/null
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2013  Christian Mollekopf <mollekopf@kolabsys.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License or (at your option) version 3 or any later version
+ * accepted by the membership of KDE e.V. (or its successor approved
+ * by the membership of KDE e.V.), which shall act as a proxy
+ * defined in Section 14 of version 3 of the license.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "migrationstatuswidget.h"
+#include "migrationscheduler.h"
+#include <QVBoxLayout>
+#include <QTreeView>
+#include <QAction>
+#include <QListView>
+#include <QLabel>
+#include <QToolBar>
+#include <QIcon>
+#include <QDialog>
+#include <KLocalizedString>
+#include <QDialogButtonBox>
+
+MigrationStatusWidget::MigrationStatusWidget(MigrationScheduler &scheduler, QWidget *parent)
+    : QWidget(parent),
+      mScheduler(scheduler)
+{
+    QVBoxLayout *vboxLayout = new QVBoxLayout;
+    {
+        QToolBar *toolbar = new QToolBar(QStringLiteral("MigrationControlToolbar"), this);
+
+        QAction *start = toolbar->addAction(QStringLiteral("Start"));
+        start->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-start")));
+        connect(start, &QAction::triggered, this, &MigrationStatusWidget::startSelected);
+
+        QAction *pause = toolbar->addAction(QStringLiteral("Pause"));
+        pause->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-pause")));
+        connect(pause, &QAction::triggered, this, &MigrationStatusWidget::pauseSelected);
+
+        QAction *abort = toolbar->addAction(QStringLiteral("Abort"));
+        abort->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-stop")));
+        connect(abort, &QAction::triggered, this, &MigrationStatusWidget::abortSelected);
+
+        vboxLayout->addWidget(toolbar);
+    }
+    {
+        QTreeView *treeView = new QTreeView(this);
+        treeView->setModel(&mScheduler.model());
+        mSelectionModel = treeView->selectionModel();
+        Q_ASSERT(mSelectionModel);
+        //Not sure why this is required, but otherwise the view doesn't load anything from the model
+        treeView->update(QModelIndex());
+        connect(treeView, &QTreeView::doubleClicked, this, &MigrationStatusWidget::onItemActivated);
+
+        vboxLayout->addWidget(treeView);
+    }
+    setLayout(vboxLayout);
+}
+
+void MigrationStatusWidget::startSelected()
+{
+    foreach (const QModelIndex &index, mSelectionModel->selectedRows()) {
+        mScheduler.start(index.data(MigratorModel::IdentifierRole).toString());
+    }
+}
+
+void MigrationStatusWidget::pauseSelected()
+{
+    foreach (const QModelIndex &index, mSelectionModel->selectedRows()) {
+        mScheduler.pause(index.data(MigratorModel::IdentifierRole).toString());
+    }
+}
+
+void MigrationStatusWidget::abortSelected()
+{
+    foreach (const QModelIndex &index, mSelectionModel->selectedRows()) {
+        mScheduler.abort(index.data(MigratorModel::IdentifierRole).toString());
+    }
+}
+
+void MigrationStatusWidget::onItemActivated(const QModelIndex &index)
+{
+    QDialog *dlg = new QDialog(this);
+    QVBoxLayout *topLayout = new QVBoxLayout;
+    dlg->setLayout(topLayout);
+    QWidget *widget = new QWidget(dlg);
+    topLayout->addWidget(widget);
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close);
+    connect(buttonBox, &QDialogButtonBox::accepted, dlg, &QDialog::accept);
+    connect(buttonBox, &QDialogButtonBox::rejected, dlg, &QDialog::reject);
+    topLayout->addWidget(buttonBox);
+
+    QVBoxLayout *vboxLayout = new QVBoxLayout;
+    {
+        QListView *listView = new QListView(widget);
+        listView->setModel(&mScheduler.logModel(index.data(MigratorModel::IdentifierRole).toString()));
+        listView->setAutoScroll(true);
+        listView->scrollToBottom();
+        vboxLayout->addWidget(listView);
+    }
+    {
+        QHBoxLayout *hboxLayout = new QHBoxLayout;
+        QLabel *label = new QLabel(QStringLiteral("<a href=\"%1\">%2</a>").arg(index.data(MigratorModel::LogfileRole).toString()).arg(ki18n("Logfile").toString()), widget);
+        label->setOpenExternalLinks(true);
+        hboxLayout->addWidget(label);
+        hboxLayout->addStretch();
+        vboxLayout->addLayout(hboxLayout);
+    }
+    widget->setLayout(vboxLayout);
+
+    dlg->setAttribute(Qt::WA_DeleteOnClose);
+    dlg->setWindowTitle(i18nc("Title of the window displaying the log of a single migration job.", "Migration Info"));
+    dlg->resize(600, 300);
+    dlg->show();
+}
+
diff --git a/agents/migration/migrationstatuswidget.h b/agents/migration/migrationstatuswidget.h
new file mode 100644 (file)
index 0000000..dec5b9e
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2013  Christian Mollekopf <mollekopf@kolabsys.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License or (at your option) version 3 or any later version
+ * accepted by the membership of KDE e.V. (or its successor approved
+ * by the membership of KDE e.V.), which shall act as a proxy
+ * defined in Section 14 of version 3 of the license.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MIGRATIONSTATUSWIDGET_H
+#define MIGRATIONSTATUSWIDGET_H
+
+#include "migrationscheduler.h"
+#include <QWidget>
+#include <QItemSelectionModel>
+
+class MigrationStatusWidget: public QWidget
+{
+    Q_OBJECT
+public:
+    explicit MigrationStatusWidget(MigrationScheduler &scheduler, QWidget *parent = Q_NULLPTR);
+private Q_SLOTS:
+    void startSelected();
+    void pauseSelected();
+    void abortSelected();
+private:
+    MigrationScheduler &mScheduler;
+    QItemSelectionModel *mSelectionModel;
+public Q_SLOTS:
+    void onItemActivated(const QModelIndex &);
+};
+
+#endif // MIGRATIONCONFIGDIALOG_H
+
diff --git a/agents/newmailnotifier/CMakeLists.txt b/agents/newmailnotifier/CMakeLists.txt
new file mode 100644 (file)
index 0000000..12fad7b
--- /dev/null
@@ -0,0 +1,63 @@
+
+include_directories(${kdepim-runtime_BINARY_DIR})
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_newmailnotifier_agent\")
+
+add_definitions( -DQT_NO_CAST_FROM_ASCII )
+add_definitions( -DQT_NO_CAST_TO_ASCII )
+
+set(newmailnotifieragent_SRCS
+  newmailnotifiersettingsdialog.cpp
+  newmailnotifieragent.cpp
+  specialnotifierjob.cpp
+  newmailnotifierselectcollectionwidget.cpp
+  newmailnotifiershowmessagejob.cpp
+)
+
+ecm_qt_declare_logging_category(newmailnotifieragent_SRCS HEADER newmailnotifier_debug.h IDENTIFIER NEWMAILNOTIFIER_LOG CATEGORY_NAME log_newmailnotifier)
+
+kconfig_add_kcfg_files(newmailnotifieragent_SRCS
+    newmailnotifieragentsettings.kcfgc
+  )
+
+
+qt5_add_dbus_adaptor(newmailnotifieragent_SRCS org.freedesktop.Akonadi.NewMailNotifier.xml newmailnotifieragent.h NewMailNotifierAgent)
+
+
+add_executable( akonadi_newmailnotifier_agent ${newmailnotifieragent_SRCS})
+
+
+target_link_libraries( akonadi_newmailnotifier_agent
+  KF5::AkonadiCore
+  KF5::Mime
+  KF5::AkonadiMime
+  KF5::AkonadiContact
+  KF5::Codecs
+  KF5::IdentityManagement
+  KF5::NotifyConfig
+  KF5::AkonadiAgentBase
+  KF5::DBusAddons
+  KF5::XmlGui
+  KF5::Notifications
+  KF5::WindowSystem
+  KF5::Completion
+  KF5::Service
+  KF5::IconThemes
+)
+
+if (Qt5TextToSpeech_FOUND)
+    target_link_libraries(akonadi_newmailnotifier_agent
+        Qt5::TextToSpeech)
+endif()
+
+if( APPLE )
+  set_target_properties( akonadi_newmailnotifier_agent PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/Info.plist.template)
+  set_target_properties( akonadi_newmailnotifier_agent PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.newmailnotifier")
+  set_target_properties( akonadi_newmailnotifier_agent PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KDE New Mail Notifier")
+endif ()
+
+install(TARGETS akonadi_newmailnotifier_agent ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} )
+
+
+install(FILES newmailnotifieragent.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents")
+install(FILES akonadi_newmailnotifier_agent.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFY5RCDIR} )
+
diff --git a/agents/newmailnotifier/Messages.sh b/agents/newmailnotifier/Messages.sh
new file mode 100755 (executable)
index 0000000..9d815dd
--- /dev/null
@@ -0,0 +1,4 @@
+#! /bin/sh
+$EXTRACTRC `find . -name '*.kcfg'` >> rc.cpp || exit 11
+$XGETTEXT *.cpp -o $podir/akonadi_newmailnotifier_agent.pot
+rm -f rc.cpp
diff --git a/agents/newmailnotifier/TODO b/agents/newmailnotifier/TODO
new file mode 100644 (file)
index 0000000..9f04060
--- /dev/null
@@ -0,0 +1,4 @@
+for 4.12:
+---------
+- Look at https://github.com/shellscape/Gmail-Notifier-Plus/tree/master/Promotional for idea
+- Show if we can display full collection path (for the moment we can't)
diff --git a/agents/newmailnotifier/akonadi_newmailnotifier_agent.notifyrc b/agents/newmailnotifier/akonadi_newmailnotifier_agent.notifyrc
new file mode 100644 (file)
index 0000000..78a96aa
--- /dev/null
@@ -0,0 +1,137 @@
+[Global]
+IconName=kmail
+Comment=New email notify
+Comment[bg]=Уведомяване за нова поща
+Comment[bs]=Obavijest o novoj pošti
+Comment[ca]=Notificador de correu electrònic nou
+Comment[ca@valencia]=Notificador de correu electrònic nou
+Comment[cs]=Upozornění na nový e-mail
+Comment[da]=Bekendtgørelse af nye e-mails
+Comment[de]=E-Mail-Benachrichtigung
+Comment[el]=Ειδοποίηση νέας αλληλογραφίας
+Comment[en_GB]=New email notify
+Comment[es]=Nueva notificación de correo
+Comment[et]=Uue kirja teavitaja
+Comment[fi]=Ilmoitus uudesta postista
+Comment[fr]=Notification de nouveaux courriers électroniques
+Comment[ga]=Fógairt ríomhphoist nua
+Comment[gl]=Notificación de nova mensaxe.
+Comment[hu]=Új e-mail értesítés
+Comment[ia]=Notifica de nove messages de e-posta
+Comment[it]=Notifica dei nuovi messaggi di posta
+Comment[kk]=Жаңа эл.пошта туралы хабарлау
+Comment[km]=ជូនដំណឹង​អ៊ីមែល​ថ្មី
+Comment[ko]=새 메일 알림
+Comment[lt]=Naujo pašto pranešimas
+Comment[lv]=Jauna pasta paziņojums
+Comment[nb]=Varsling om ny e-post
+Comment[nds]=Bescheed över nieg Nettpost
+Comment[nl]=Melding van nieuwe e-mail
+Comment[pl]=Powiadomienie o nowej poczcie
+Comment[pt]=Notificação de correio novo
+Comment[pt_BR]=Notificação de novo e-mail
+Comment[ru]=Уведомление о новой почте
+Comment[sk]=Oznámenie novej pošty
+Comment[sl]=Obvestilo o novi e-pošti
+Comment[sr]=Обавештење о пристиглој е‑пошти
+Comment[sr@ijekavian]=Обавештење о пристиглој е‑пошти
+Comment[sr@ijekavianlatin]=Obaveštenje o pristigloj e‑pošti
+Comment[sr@latin]=Obaveštenje o pristigloj e‑pošti
+Comment[sv]=Ny brevunderrättelse
+Comment[tr]=Yeni e-posta bildirimi
+Comment[uk]=Сповіщувач про нові повідомлення
+Comment[x-test]=xxNew email notifyxx
+Comment[zh_CN]=新邮件提醒
+Comment[zh_TW]=新郵件通知
+Name=New email notify
+Name[bg]=Уведомяване за нова поща
+Name[bs]=Obavijest o novoj pošti
+Name[ca]=Notificador de correu electrònic nou
+Name[ca@valencia]=Notificador de correu electrònic nou
+Name[cs]=Upozornění na nový e-mail
+Name[da]=Bekendtgørelse af nye e-mails
+Name[de]=E-Mail-Benachrichtigung
+Name[el]=Ειδοποίηση νέας αλληλογραφίας
+Name[en_GB]=New email notify
+Name[es]=Nueva notificación de correo
+Name[et]=Uue kirja teavitaja
+Name[fi]=Ilmoitus uudesta postista
+Name[fr]=Notification de nouveaux courriers électroniques
+Name[ga]=Fógairt ríomhphoist nua
+Name[gl]=Notificación de nova mensaxe
+Name[hu]=Új e-mail értesítés
+Name[ia]=Notifica de nove messages de e-posta
+Name[it]=Notifica dei nuovi messaggi di posta
+Name[kk]=Жаңа эл.пошта туралы хабарлау
+Name[km]=ជូនដំណឹង​អ៊ីមែល​ថ្មី
+Name[ko]=새 메일 알림
+Name[lt]=Naujo pašto pranešimas
+Name[lv]=Jauna pasta paziņojums
+Name[nb]=Varsling om ny e-post
+Name[nds]=Nieg Nettpost
+Name[nl]=Melding van nieuwe e-mail
+Name[pl]=Powiadomienie o nowej poczcie
+Name[pt]=Notificação de correio novo
+Name[pt_BR]=Notificação de novo e-mail
+Name[ro]=Notificare mesaje noi
+Name[ru]=Уведомление о новой почте
+Name[sk]=Oznámenie novej pošty
+Name[sl]=Obvestilo o novi e-pošti
+Name[sr]=Обавештење о пристиглој е‑пошти
+Name[sr@ijekavian]=Обавештење о пристиглој е‑пошти
+Name[sr@ijekavianlatin]=Obaveštenje o pristigloj e‑pošti
+Name[sr@latin]=Obaveštenje o pristigloj e‑pošti
+Name[sv]=Ny brevunderrättelse
+Name[tr]=Yeni e-posta bildirimi
+Name[uk]=Сповіщувач про нові повідомлення
+Name[x-test]=xxNew email notifyxx
+Name[zh_CN]=新邮件提醒
+Name[zh_TW]=新郵件通知
+
+[Event/new-email]
+Name=New email arrived
+Name[bg]=Пристигна нова поща
+Name[bs]=Nova pošta stigla
+Name[ca]=Ha arribat correu electrònic nou
+Name[ca@valencia]=Ha arribat correu electrònic nou
+Name[cs]=Přišel nový e-mail
+Name[da]=Ny e-mail ankommet
+Name[de]=Neue Nachrichten sind eingetroffen
+Name[el]=Ελήφθη νέα αλληλογραφία
+Name[en_GB]=New email arrived
+Name[es]=Llegó correo nuevo
+Name[et]=Saabus uus kiri
+Name[fi]=Uutta postia saapunut
+Name[fr]=Un nouveau courrier électronique est arrivé
+Name[ga]=Tháinig ríomhphost nua
+Name[gl]=Chegou unha mensaxe nova
+Name[hu]=Új e-mail érkezett
+Name[ia]=Nove message de e-posta arrivava
+Name[it]=Nuova posta ricevuta
+Name[kk]=Жаңа пошта келді
+Name[km]=មាន​អ៊ីមែល​ថ្មី​មក​ដល់
+Name[ko]=새 메일이 도착했습니다
+Name[lt]=Naujas laiškas gautas
+Name[lv]=Saņemts jauns e-pasts
+Name[nb]=Ny e-post ankommet
+Name[nds]=Niege Nettpost ankamen
+Name[nl]=Nieuwe e-mail aangekomen
+Name[pl]=Nadeszła nowa poczta
+Name[pt]=Notificação de correio novo
+Name[pt_BR]=Notificação de novo e-mail
+Name[ro]=Mesaje noi sosite
+Name[ru]=Получена новая почта
+Name[sk]=Prišla nová pošta
+Name[sl]=Prispela je nova e-pošta
+Name[sr]=Стигла је нова е‑пошта
+Name[sr@ijekavian]=Стигла је нова е‑пошта
+Name[sr@ijekavianlatin]=Stigla je nova e‑pošta
+Name[sr@latin]=Stigla je nova e‑pošta
+Name[sv]=Ny post har anlänt
+Name[tr]=Yeni e-posta alındı
+Name[uk]=Надійшла нова пошта
+Name[x-test]=xxNew email arrivedxx
+Name[zh_CN]=新邮件到达
+Name[zh_TW]=新郵件已抵達
+Action=Popup
+
diff --git a/agents/newmailnotifier/newmailnotifieragent.cpp b/agents/newmailnotifier/newmailnotifieragent.cpp
new file mode 100644 (file)
index 0000000..b164e81
--- /dev/null
@@ -0,0 +1,575 @@
+/*
+    Copyright (c) 2013-2015 Laurent Montel <montel@kde.org>
+
+    Copyright (c) 2010 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "newmailnotifieragent.h"
+
+#include <AkonadiCore/NewMailNotifierAttribute>
+#include "specialnotifierjob.h"
+#include "newmailnotifieradaptor.h"
+#include "newmailnotifieragentsettings.h"
+#include "newmailnotifiersettingsdialog.h"
+
+#include <KIdentityManagement/IdentityManager>
+
+#include <kdbusconnectionpool.h>
+
+#include <changerecorder.h>
+#include <entitydisplayattribute.h>
+#include <entityhiddenattribute.h>
+#include <itemfetchscope.h>
+#include <session.h>
+#include <AttributeFactory>
+#include <CollectionFetchScope>
+#include <Akonadi/KMime/SpecialMailCollections>
+#include <Akonadi/KMime/MessageStatus>
+#include <AgentManager>
+#include <KLocalizedString>
+#include <KMime/Message>
+#include <KNotification>
+#include <KWindowSystem>
+#include <Kdelibs4ConfigMigrator>
+#include "newmailnotifier_debug.h"
+#include <KToolInvocation>
+#include <QIcon>
+#include <KIconLoader>
+
+using namespace Akonadi;
+
+NewMailNotifierAgent::NewMailNotifierAgent(const QString &id)
+    : AgentBase(id)
+{
+    Kdelibs4ConfigMigrator migrate(QStringLiteral("newmailnotifieragent"));
+    migrate.setConfigFiles(QStringList() << QStringLiteral("akonadi_newmailnotifier_agentrc") << QStringLiteral("akonadi_newmailnotifier_agent.notifyrc"));
+    migrate.migrate();
+
+    KLocalizedString::setApplicationDomain("akonadi_newmailnotifier_agent");
+    Akonadi::AttributeFactory::registerAttribute<Akonadi::NewMailNotifierAttribute>();
+    new NewMailNotifierAdaptor(this);
+
+    mIdentityManager = new KIdentityManagement::IdentityManager(false, this);
+    connect(mIdentityManager, SIGNAL(changed()), SLOT(slotIdentitiesChanged()));
+    slotIdentitiesChanged();
+    mDefaultPixmap = QIcon::fromTheme(QStringLiteral("kmail")).pixmap(KIconLoader::SizeMedium, KIconLoader::SizeMedium);
+
+    KDBusConnectionPool::threadConnection().registerObject(QStringLiteral("/NewMailNotifierAgent"),
+            this, QDBusConnection::ExportAdaptors);
+    KDBusConnectionPool::threadConnection().registerService(QStringLiteral("org.freedesktop.Akonadi.NewMailNotifierAgent"));
+
+    connect(Akonadi::AgentManager::self(), &Akonadi::AgentManager::instanceStatusChanged, this, &NewMailNotifierAgent::slotInstanceStatusChanged);
+    connect(Akonadi::AgentManager::self(), &Akonadi::AgentManager::instanceRemoved, this, &NewMailNotifierAgent::slotInstanceRemoved);
+    connect(Akonadi::AgentManager::self(), &Akonadi::AgentManager::instanceAdded, this, &NewMailNotifierAgent::slotInstanceAdded);
+    connect(Akonadi::AgentManager::self(), &Akonadi::AgentManager::instanceNameChanged, this, &NewMailNotifierAgent::slotInstanceNameChanged);
+
+    changeRecorder()->setMimeTypeMonitored(KMime::Message::mimeType());
+    changeRecorder()->itemFetchScope().setCacheOnly(true);
+    changeRecorder()->itemFetchScope().setFetchModificationTime(false);
+    changeRecorder()->fetchCollection(true);
+    changeRecorder()->setChangeRecordingEnabled(false);
+    changeRecorder()->ignoreSession(Akonadi::Session::defaultSession());
+    changeRecorder()->collectionFetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All);
+    changeRecorder()->setCollectionMonitored(Collection::root(), true);
+    mTimer.setInterval(5 * 1000);
+    connect(&mTimer, &QTimer::timeout, this, &NewMailNotifierAgent::slotShowNotifications);
+
+    if (isActive()) {
+        mTimer.setSingleShot(true);
+    }
+}
+
+void NewMailNotifierAgent::slotIdentitiesChanged()
+{
+    mListEmails = mIdentityManager->allEmails();
+}
+
+void NewMailNotifierAgent::doSetOnline(bool online)
+{
+    if (!online) {
+        clearAll();
+    }
+}
+
+void NewMailNotifierAgent::setExcludeMyselfFromNotification(bool b)
+{
+    NewMailNotifierAgentSettings::setExcludeEmailsFromMe(b);
+    NewMailNotifierAgentSettings::self()->save();
+}
+
+bool NewMailNotifierAgent::excludeMyselfFromNotification() const
+{
+    return NewMailNotifierAgentSettings::excludeEmailsFromMe();
+}
+
+void NewMailNotifierAgent::setShowPhoto(bool show)
+{
+    NewMailNotifierAgentSettings::setShowPhoto(show);
+    NewMailNotifierAgentSettings::self()->save();
+}
+
+bool NewMailNotifierAgent::showPhoto() const
+{
+    return NewMailNotifierAgentSettings::showPhoto();
+}
+
+void NewMailNotifierAgent::setShowFrom(bool show)
+{
+    NewMailNotifierAgentSettings::setShowFrom(show);
+    NewMailNotifierAgentSettings::self()->save();
+}
+
+bool NewMailNotifierAgent::showFrom() const
+{
+    return NewMailNotifierAgentSettings::showFrom();
+}
+
+void NewMailNotifierAgent::setShowSubject(bool show)
+{
+    NewMailNotifierAgentSettings::setShowSubject(show);
+    NewMailNotifierAgentSettings::self()->save();
+}
+
+bool NewMailNotifierAgent::showSubject() const
+{
+    return NewMailNotifierAgentSettings::showSubject();
+}
+
+void NewMailNotifierAgent::setShowFolderName(bool show)
+{
+    NewMailNotifierAgentSettings::setShowFolder(show);
+    NewMailNotifierAgentSettings::self()->save();
+}
+
+bool NewMailNotifierAgent::showFolderName() const
+{
+    return NewMailNotifierAgentSettings::showFolder();
+}
+
+void NewMailNotifierAgent::setEnableAgent(bool enabled)
+{
+    NewMailNotifierAgentSettings::setEnabled(enabled);
+    NewMailNotifierAgentSettings::self()->save();
+    if (!enabled) {
+        clearAll();
+    }
+}
+
+void NewMailNotifierAgent::setVerboseMailNotification(bool verbose)
+{
+    NewMailNotifierAgentSettings::setVerboseNotification(verbose);
+    NewMailNotifierAgentSettings::self()->save();
+}
+
+bool NewMailNotifierAgent::verboseMailNotification() const
+{
+    return NewMailNotifierAgentSettings::verboseNotification();
+}
+
+void NewMailNotifierAgent::setTextToSpeakEnabled(bool enabled)
+{
+    NewMailNotifierAgentSettings::setTextToSpeakEnabled(enabled);
+    NewMailNotifierAgentSettings::self()->save();
+}
+
+bool NewMailNotifierAgent::textToSpeakEnabled() const
+{
+    return NewMailNotifierAgentSettings::textToSpeakEnabled();
+}
+
+void NewMailNotifierAgent::setTextToSpeak(const QString &msg)
+{
+    NewMailNotifierAgentSettings::setTextToSpeak(msg);
+    NewMailNotifierAgentSettings::self()->save();
+}
+
+QString NewMailNotifierAgent::textToSpeak() const
+{
+    return NewMailNotifierAgentSettings::textToSpeak();
+}
+
+void NewMailNotifierAgent::clearAll()
+{
+    mNewMails.clear();
+    mInstanceNameInProgress.clear();
+}
+
+bool NewMailNotifierAgent::enabledAgent() const
+{
+    return NewMailNotifierAgentSettings::enabled();
+}
+
+bool NewMailNotifierAgent::showButtonToDisplayMail() const
+{
+    return NewMailNotifierAgentSettings::showButtonToDisplayMail();
+}
+
+void NewMailNotifierAgent::setShowButtonToDisplayMail(bool b)
+{
+    NewMailNotifierAgentSettings::setShowButtonToDisplayMail(b);
+    NewMailNotifierAgentSettings::self()->save();
+}
+
+void NewMailNotifierAgent::showConfigureDialog(qlonglong windowId)
+{
+    configure(windowId);
+}
+
+void NewMailNotifierAgent::configure(WId windowId)
+{
+    QPointer<NewMailNotifierSettingsDialog> dialog = new NewMailNotifierSettingsDialog;
+    if (windowId) {
+#ifndef Q_OS_WIN
+        KWindowSystem::setMainWindow(dialog, windowId);
+#else
+        KWindowSystem::setMainWindow(dialog, (HWND)windowId);
+#endif
+    }
+    dialog->exec();
+    delete dialog;
+}
+
+bool NewMailNotifierAgent::excludeSpecialCollection(const Akonadi::Collection &collection) const
+{
+    if (collection.hasAttribute<Akonadi::EntityHiddenAttribute>()) {
+        return true;
+    }
+
+    if (collection.hasAttribute<Akonadi::NewMailNotifierAttribute>()) {
+        if (collection.attribute<Akonadi::NewMailNotifierAttribute>()->ignoreNewMail()) {
+            return true;
+        }
+    }
+
+    if (!collection.contentMimeTypes().contains(KMime::Message::mimeType())) {
+        return true;
+    }
+
+    SpecialMailCollections::Type type = SpecialMailCollections::self()->specialCollectionType(collection);
+    switch (type) {
+    case SpecialMailCollections::Invalid: //Not a special collection
+    case SpecialMailCollections::Inbox:
+        return false;
+    default:
+        return true;
+    }
+
+}
+
+void NewMailNotifierAgent::itemsRemoved(const Item::List &items)
+{
+    if (!isActive()) {
+        return;
+    }
+
+    QHash< Akonadi::Collection, QList<Akonadi::Item::Id> >::iterator end(mNewMails.end());
+    for (QHash< Akonadi::Collection, QList<Akonadi::Item::Id> >::iterator it = mNewMails.begin(); it != end; ++it) {
+        QList<Akonadi::Item::Id> idList = it.value();
+        bool itemFound = false;
+        Q_FOREACH (const Item &item, items) {
+            if (idList.contains(item.id())) {
+                idList.removeAll(item.id());
+                itemFound = true;
+            }
+        }
+        if (itemFound) {
+            if (mNewMails[it.key()].isEmpty()) {
+                mNewMails.remove(it.key());
+            } else {
+                mNewMails[it.key()] = idList;
+            }
+        }
+    }
+}
+
+void NewMailNotifierAgent::itemsFlagsChanged(const Akonadi::Item::List &items, const QSet<QByteArray> &addedFlags, const QSet<QByteArray> &removedFlags)
+{
+    if (!isActive()) {
+        return;
+    }
+    Q_FOREACH (const Akonadi::Item &item, items) {
+        QHash< Akonadi::Collection, QList<Akonadi::Item::Id> >::iterator end(mNewMails.end());
+        for (QHash< Akonadi::Collection, QList<Akonadi::Item::Id> >::iterator it = mNewMails.begin(); it != end; ++it) {
+            QList<Akonadi::Item::Id> idList = it.value();
+            if (idList.contains(item.id()) && addedFlags.contains("\\SEEN")) {
+                idList.removeAll(item.id());
+                if (idList.isEmpty()) {
+                    mNewMails.remove(it.key());
+                    break;
+                } else {
+                    (*it) = idList;
+                }
+            }
+        }
+    }
+}
+
+void NewMailNotifierAgent::itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &collectionSource, const Akonadi::Collection &collectionDestination)
+{
+    if (!isActive()) {
+        return;
+    }
+
+    Q_FOREACH (const Akonadi::Item &item, items) {
+        if (ignoreStatusMail(item)) {
+            continue;
+        }
+
+        if (excludeSpecialCollection(collectionSource)) {
+            continue; // outbox, sent-mail, trash, drafts or templates.
+        }
+
+        if (mNewMails.contains(collectionSource)) {
+            QList<Akonadi::Item::Id> idListFrom = mNewMails[ collectionSource ];
+            if (idListFrom.contains(item.id())) {
+                idListFrom.removeAll(item.id());
+
+                if (idListFrom.isEmpty()) {
+                    mNewMails.remove(collectionSource);
+                } else {
+                    mNewMails[ collectionSource ] = idListFrom;
+                }
+                if (!excludeSpecialCollection(collectionDestination)) {
+                    QList<Akonadi::Item::Id> idListTo = mNewMails[ collectionDestination ];
+                    idListTo.append(item.id());
+                    mNewMails[ collectionDestination ] = idListTo;
+                }
+            }
+        }
+    }
+}
+
+bool NewMailNotifierAgent::ignoreStatusMail(const Akonadi::Item &item)
+{
+    Akonadi::MessageStatus status;
+    status.setStatusFromFlags(item.flags());
+    if (status.isRead() || status.isSpam() || status.isIgnored()) {
+        return true;
+    }
+    return false;
+}
+
+void NewMailNotifierAgent::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection)
+{
+    if (!isActive()) {
+        return;
+    }
+
+    if (excludeSpecialCollection(collection)) {
+        return; // outbox, sent-mail, trash, drafts or templates.
+    }
+
+    if (ignoreStatusMail(item)) {
+        return;
+    }
+
+    if (!mTimer.isActive()) {
+        mTimer.start();
+    }
+    mNewMails[ collection ].append(item.id());
+}
+
+void NewMailNotifierAgent::slotShowNotifications()
+{
+    if (mNewMails.isEmpty()) {
+        return;
+    }
+
+    if (!isActive()) {
+        return;
+    }
+
+    if (!mInstanceNameInProgress.isEmpty()) {
+        //Restart timer until all is done.
+        mTimer.start();
+        return;
+    }
+
+    QString message;
+    if (NewMailNotifierAgentSettings::verboseNotification()) {
+        bool hasUniqMessage = true;
+        Akonadi::Item::Id item = -1;
+        QString currentPath;
+        QStringList texts;
+        QHash< Akonadi::Collection, QList<Akonadi::Item::Id> >::const_iterator end(mNewMails.constEnd());
+        const int numberOfCollection(mNewMails.count());
+        if (numberOfCollection > 1) {
+            hasUniqMessage = false;
+        }
+
+        for (QHash< Akonadi::Collection, QList<Akonadi::Item::Id> >::const_iterator it = mNewMails.constBegin(); it != end; ++it) {
+            Akonadi::EntityDisplayAttribute *attr = it.key().attribute<Akonadi::EntityDisplayAttribute>();
+            QString displayName;
+            if (attr && !attr->displayName().isEmpty()) {
+                displayName = attr->displayName();
+            } else {
+                displayName = it.key().name();
+            }
+
+            if (hasUniqMessage) {
+                if (it.value().count() == 0) {
+                    //You can have an unique folder with 0 message
+                    return;
+                } else if (it.value().count() == 1) {
+                    item = it.value().at(0);
+                    currentPath = displayName;
+                    break;
+                } else {
+                    hasUniqMessage = false;
+                }
+            }
+            QString resourceName;
+            if (!mCacheResourceName.contains(it.key().resource())) {
+                Q_FOREACH (const Akonadi::AgentInstance &instance, Akonadi::AgentManager::self()->instances()) {
+                    if (instance.identifier() == it.key().resource()) {
+                        mCacheResourceName.insert(instance.identifier(), instance.name());
+                        resourceName = instance.name();
+                        break;
+                    }
+                }
+            } else {
+                resourceName = mCacheResourceName.value(it.key().resource());
+            }
+            const int numberOfEmails(it.value().count());
+            if (numberOfEmails > 0) {
+                texts.append(i18ncp("%2 = name of mail folder; %3 = name of Akonadi POP3/IMAP/etc resource (as user named it)",
+                                    "One new email in %2 from \"%3\"",
+                                    "%1 new emails in %2 from \"%3\"", numberOfEmails, displayName,
+                                    resourceName));
+            }
+        }
+        if (hasUniqMessage) {
+            SpecialNotifierJob *job = new SpecialNotifierJob(mListEmails, currentPath, item, this);
+            job->setDefaultPixmap(mDefaultPixmap);
+            connect(job, &SpecialNotifierJob::displayNotification, this, &NewMailNotifierAgent::slotDisplayNotification);
+
+            mNewMails.clear();
+            return;
+        } else {
+            message = texts.join(QStringLiteral("<br>"));
+        }
+    } else {
+        message = i18n("New mail arrived");
+    }
+
+    qCDebug(NEWMAILNOTIFIER_LOG) << message;
+
+    slotDisplayNotification(mDefaultPixmap, message);
+
+    mNewMails.clear();
+}
+
+void NewMailNotifierAgent::slotDisplayNotification(const QPixmap &pixmap, const QString &message)
+{
+    KNotification::event(QStringLiteral("new-email"),
+                         message,
+                         pixmap,
+                         Q_NULLPTR,
+                         KNotification::CloseOnTimeout,
+                         QStringLiteral("akonadi_newmailnotifier_agent"));
+
+}
+
+void NewMailNotifierAgent::slotInstanceNameChanged(const Akonadi::AgentInstance &instance)
+{
+    if (!isActive()) {
+        return;
+    }
+
+    const QString identifier(instance.identifier());
+    if (mCacheResourceName.contains(identifier)) {
+        mCacheResourceName.remove(identifier);
+        mCacheResourceName.insert(identifier, instance.name());
+    }
+}
+
+void NewMailNotifierAgent::slotInstanceStatusChanged(const Akonadi::AgentInstance &instance)
+{
+    if (!isActive()) {
+        return;
+    }
+
+    const QString identifier(instance.identifier());
+    switch (instance.status()) {
+    case Akonadi::AgentInstance::Broken:
+    case Akonadi::AgentInstance::Idle: {
+        if (mInstanceNameInProgress.contains(identifier)) {
+            mInstanceNameInProgress.removeAll(identifier);
+        }
+        break;
+    }
+    case Akonadi::AgentInstance::Running: {
+        if (!excludeAgentType(instance)) {
+            if (!mInstanceNameInProgress.contains(identifier)) {
+                mInstanceNameInProgress.append(identifier);
+            }
+        }
+        break;
+    }
+    case Akonadi::AgentInstance::NotConfigured:
+        //Nothing
+        break;
+    }
+}
+
+bool NewMailNotifierAgent::excludeAgentType(const Akonadi::AgentInstance &instance)
+{
+    if (instance.type().mimeTypes().contains(KMime::Message::mimeType())) {
+        const QStringList capabilities(instance.type().capabilities());
+        if (capabilities.contains(QStringLiteral("Resource")) &&
+                !capabilities.contains(QStringLiteral("Virtual")) &&
+                !capabilities.contains(QStringLiteral("MailTransport"))) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+    return true;
+}
+
+void NewMailNotifierAgent::slotInstanceRemoved(const Akonadi::AgentInstance &instance)
+{
+    if (!isActive()) {
+        return;
+    }
+
+    const QString identifier(instance.identifier());
+    if (mInstanceNameInProgress.contains(identifier)) {
+        mInstanceNameInProgress.removeAll(identifier);
+    }
+}
+
+void NewMailNotifierAgent::slotInstanceAdded(const Akonadi::AgentInstance &instance)
+{
+    mCacheResourceName.insert(instance.identifier(), instance.name());
+}
+
+void NewMailNotifierAgent::printDebug()
+{
+    qCDebug(NEWMAILNOTIFIER_LOG) << "instance in progress: " << mInstanceNameInProgress
+                                 << "\n notifier enabled : " << NewMailNotifierAgentSettings::enabled()
+                                 << "\n check in progress : " << !mInstanceNameInProgress.isEmpty();
+}
+
+bool NewMailNotifierAgent::isActive() const
+{
+    return isOnline() && NewMailNotifierAgentSettings::enabled();
+}
+
+AKONADI_AGENT_MAIN(NewMailNotifierAgent)
+
diff --git a/agents/newmailnotifier/newmailnotifieragent.desktop b/agents/newmailnotifier/newmailnotifieragent.desktop
new file mode 100644 (file)
index 0000000..9bffe6b
--- /dev/null
@@ -0,0 +1,97 @@
+[Desktop Entry]
+Name=New Email Notifier
+Name[bg]=Уведомяване за нова поща
+Name[bs]=Novi obavještavač o pošti
+Name[ca]=Notificador de correu electrònic nou
+Name[ca@valencia]=Notificador de correu electrònic nou
+Name[cs]=Upozornění na nový e-mail
+Name[da]=Bekendtgørelse af nye e-mails
+Name[de]=E-Mail-Benachrichtigung
+Name[el]=Ειδοποιητής νέας αλληλογραφίας
+Name[en_GB]=New Email Notifier
+Name[es]=Nuevo notificador de correo
+Name[et]=Uue kirja teavitaja
+Name[fi]=Uudesta postista ilmoitin
+Name[fr]=Notification de nouveaux courriers électroniques
+Name[ga]=Fógra Ríomhphoist Nua
+Name[gl]=Novo notificador de correo electrónico
+Name[hu]=Új e-mail értesítő
+Name[ia]=Notificator de nove messages de e-posta
+Name[it]=Notifiche dei nuovi messaggi di posta
+Name[kk]=Жаңа эл.пошта туралы хабарлау
+Name[km]=កម្មវិធី​ជូន​ដំណឹង​អ៊ីមែល​ថ្មី
+Name[ko]=새 메일 알리미
+Name[lt]=Naujo pašto pranešėjas
+Name[lv]=Jauna pasta paziņotājs
+Name[nb]=Varsling om ny e-post
+Name[nds]=Nieg-Nettpost-Bescheedgever
+Name[nl]=Nieuwe e-mailmelder
+Name[pa]=ਨਵੀਂ ਈਮੇਲ ਸੂਚਨਾ
+Name[pl]=Powiadomienie o nowej poczcie
+Name[pt]=Notificação de Correio Novo
+Name[pt_BR]=Notificação de novo e-mail
+Name[ro]=Notificator mesaje noi
+Name[ru]=Уведомления о новой почте
+Name[sk]=Oznamovač novej pošty
+Name[sl]=Obvestilnik o novi e-pošti
+Name[sr]=Извештавач о пристиглој е‑пошти
+Name[sr@ijekavian]=Извештавач о пристиглој е‑пошти
+Name[sr@ijekavianlatin]=Izveštavač o pristigloj e‑pošti
+Name[sr@latin]=Izveštavač o pristigloj e‑pošti
+Name[sv]=Ny brevunderrättelse
+Name[tr]=Yeni E-posta Bildirimi
+Name[uk]=Сповіщувач про нові повідомлення
+Name[x-test]=xxNew Email Notifierxx
+Name[zh_CN]=新邮件提醒
+Name[zh_TW]=新郵件通知器
+Comment=Notifications about newly received emails
+Comment[ast]=Avisos tocante a correos nuevos recibíos
+Comment[bs]=Obavještenja o novoprimljenoj pošti
+Comment[ca]=Notificacions quant a correus electrònics nous rebuts
+Comment[ca@valencia]=Notificacions quant a correus electrònics nous rebuts
+Comment[cs]=Oznamování nově příchozích e-mailů
+Comment[da]=Bekendtgørelser om nyligt modtagne e-mails
+Comment[de]=Benachrichtigungen über neu empfangene E-Mails
+Comment[el]=Ειδοποιήσεις για νέα αλληλογραφία
+Comment[en_GB]=Notifications about newly received emails
+Comment[es]=Notificaciones sobre correos recibidos recientemente
+Comment[et]=Märguanded äsja saabunud e-kirjade kohta
+Comment[fi]=Ilmoitukset saapuneesta uudesta sähköpostista
+Comment[fr]=Notifications à propos des courriers électroniques récemment reçus
+Comment[ga]=Fógraí faoi ríomhphost nua
+Comment[gl]=Notificacións de mensaxes acabadas de chegar
+Comment[hu]=Értesítések újonnan érkezett e-mailekről
+Comment[ia]=Notificationes re nove e-postas recipite
+Comment[it]=Notifiche dei messaggi di posta elettronica ricevuti recentemente
+Comment[kk]=Жаңа эл.пошта келгені туралы хабарлау
+Comment[km]=ការ​ជូន​ដំណឹង​អំពី​អ៊ីមែល​​​ដែល​បាន​ទទួល​ថ្មីៗ
+Comment[ko]=새로 받은 메일 알림
+Comment[lt]=Pranešimai apie naujai atsiųstus el. laiškus
+Comment[lv]=Paziņojumi par jauniem e-pastiem
+Comment[nb]=Varslinger om nylig mottatte e-poster
+Comment[nds]=Bescheden över nieg rinkamen Nettbreven
+Comment[nl]=Meldingen over nieuw ontvangen e-mails
+Comment[pl]=Powiadomienia o nowo otrzymanych wiadomościach
+Comment[pt]=Notificações acerca do correio novo recebido
+Comment[pt_BR]=Notificações sobre os novos e-mails recebidos
+Comment[ro]=Notificări despre mesajele noi
+Comment[ru]=Уведомления о получении новых электронных писем
+Comment[sk]=Notifikácie o novo prijatých e-mailoch
+Comment[sl]=Obvestila o na novo prispeli e-pošti
+Comment[sr]=Обавештења о недавно пристиглој е‑пошти
+Comment[sr@ijekavian]=Обавештења о недавно пристиглој е‑пошти
+Comment[sr@ijekavianlatin]=Obaveštenja o nedavno pristigloj e‑pošti
+Comment[sr@latin]=Obaveštenja o nedavno pristigloj e‑pošti
+Comment[sv]=Underrättelser om nyss mottagen e-post
+Comment[tr]=Yeni gelen e-postalar için bildirimler
+Comment[uk]=Сповіщення щодо щойно отриманих повідомлень
+Comment[x-test]=xxNotifications about newly received emailsxx
+Comment[zh_CN]=新邮件通知
+Comment[zh_TW]=收到新郵件的通知
+Icon=mail-unread-new
+Type=AkonadiAgent
+Exec=akonadi_newmailnotifier_agent
+
+X-Akonadi-MimeTypes=message/rfc822
+X-Akonadi-Capabilities=Unique,Autostart
+X-Akonadi-Identifier=akonadi_newmailnotifier_agent
diff --git a/agents/newmailnotifier/newmailnotifieragent.h b/agents/newmailnotifier/newmailnotifieragent.h
new file mode 100644 (file)
index 0000000..fe0f152
--- /dev/null
@@ -0,0 +1,117 @@
+/*
+    Copyright (c) 2013-2015 Laurent Montel <montel@kde.org>
+
+    Copyright (c) 2010 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef NEWMAILNOTIFIERAGENT_H
+#define NEWMAILNOTIFIERAGENT_H
+
+#include <collection.h> // make sure this is included before QHash, otherwise it wont find the correct qHash implementation for some reason
+#include <agentbase.h>
+
+#include <QTimer>
+#include <QStringList>
+#include <QPixmap>
+namespace Akonadi
+{
+class AgentInstance;
+}
+
+namespace KIdentityManagement
+{
+class IdentityManager;
+}
+
+class NewMailNotifierAgent : public Akonadi::AgentBase, public Akonadi::AgentBase::ObserverV3
+{
+    Q_OBJECT
+
+public:
+    explicit NewMailNotifierAgent(const QString &id);
+
+    void showConfigureDialog(qlonglong windowId = 0);
+
+    void setEnableAgent(bool b);
+    bool enabledAgent() const;
+
+    void setVerboseMailNotification(bool b);
+    bool verboseMailNotification() const;
+
+    void setBeepOnNewMails(bool b);
+    bool beepOnNewMails() const;
+
+    void setShowPhoto(bool b);
+    bool showPhoto() const;
+
+    void setShowFrom(bool b);
+    bool showFrom() const;
+
+    void setShowSubject(bool b);
+    bool showSubject() const;
+
+    void setShowFolderName(bool b);
+    bool showFolderName() const;
+
+    void setExcludeMyselfFromNotification(bool b);
+    bool excludeMyselfFromNotification() const;
+
+    void setTextToSpeakEnabled(bool enabled);
+    bool textToSpeakEnabled() const;
+
+    QString textToSpeak() const;
+    void setTextToSpeak(const QString &msg);
+
+    void printDebug();
+
+    bool showButtonToDisplayMail() const;
+    void setShowButtonToDisplayMail(bool b);
+
+protected:
+    void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    void itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &sourceCollection, const Akonadi::Collection &destinationCollection) Q_DECL_OVERRIDE;
+    void itemsRemoved(const Akonadi::Item::List &items) Q_DECL_OVERRIDE;
+    void itemsFlagsChanged(const Akonadi::Item::List &items, const QSet<QByteArray> &addedFlags, const QSet<QByteArray> &removedFlags) Q_DECL_OVERRIDE;
+    void doSetOnline(bool online) Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void slotShowNotifications();
+    void configure(WId windowId) Q_DECL_OVERRIDE;
+    void slotInstanceStatusChanged(const Akonadi::AgentInstance &instance);
+    void slotInstanceRemoved(const Akonadi::AgentInstance &instance);
+    void slotInstanceAdded(const Akonadi::AgentInstance &instance);
+    void slotDisplayNotification(const QPixmap &pixmap, const QString &message);
+    void slotIdentitiesChanged();
+    void slotInstanceNameChanged(const Akonadi::AgentInstance &instance);
+
+private:
+    bool excludeAgentType(const Akonadi::AgentInstance &instance);
+    bool ignoreStatusMail(const Akonadi::Item &item);
+    bool isActive() const;
+    void clearAll();
+    bool excludeSpecialCollection(const Akonadi::Collection &collection) const;
+    QPixmap mDefaultPixmap;
+    QStringList mListEmails;
+    QHash<Akonadi::Collection, QList<Akonadi::Item::Id> > mNewMails;
+    QHash<QString, QString> mCacheResourceName;
+    QTimer mTimer;
+    QStringList mInstanceNameInProgress;
+    KIdentityManagement::IdentityManager *mIdentityManager;
+};
+
+#endif
diff --git a/agents/newmailnotifier/newmailnotifieragentsettings.kcfg b/agents/newmailnotifier/newmailnotifieragentsettings.kcfg
new file mode 100644 (file)
index 0000000..7546b57
--- /dev/null
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
+      http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
+ <include>KLocalizedString</include>
+ <group name="General">
+ <entry name="verboseNotification" key="verboseNotification" type="Bool">
+   <default>true</default>
+ </entry>
+ <entry name="enabled" key="enabled" type="Bool">
+   <default>true</default>
+ </entry>
+ <entry name="showPhoto" key="showPhoto" type="Bool">
+   <default>true</default>
+ </entry>
+ <entry name="showFrom" key="showFrom" type="Bool">
+   <default>true</default>
+ </entry>
+ <entry name="showSubject" key="showSubject" type="Bool">
+   <default>true</default>
+ </entry>
+ <entry name="showFolder" key="showFolder" type="Bool">
+   <default>true</default>
+ </entry>
+ <entry name="excludeEmailsFromMe" key="excludeEmailsFromMe" type="Bool">
+   <default>false</default>
+ </entry>
+ <entry name="textToSpeakEnabled" key="textToSpeakEnabled" type="Bool">
+   <default>false</default>
+ </entry>
+ <entry name="textToSpeak" key="textToSpeak" type="String">
+   <default code="true">i18nc("%s is a variable for agent. Do not change it", "A message was received from %s")</default>
+ </entry>
+
+ <entry name="showButtonToDisplayMail" key="showButtonToDisplayMail" type="Bool">
+   <default>false</default>
+ </entry>
+ </group>
+</kcfg>
diff --git a/agents/newmailnotifier/newmailnotifieragentsettings.kcfgc b/agents/newmailnotifier/newmailnotifieragentsettings.kcfgc
new file mode 100644 (file)
index 0000000..e7f0244
--- /dev/null
@@ -0,0 +1,6 @@
+# Code generation options for kconfig_compiler
+File=newmailnotifieragentsettings.kcfg
+ClassName=NewMailNotifierAgentSettings
+Singleton=true
+Mutators=true
+SetUserTexts=true
diff --git a/agents/newmailnotifier/newmailnotifierselectcollectionwidget.cpp b/agents/newmailnotifier/newmailnotifierselectcollectionwidget.cpp
new file mode 100644 (file)
index 0000000..13d5378
--- /dev/null
@@ -0,0 +1,218 @@
+/*
+    Copyright (c) 2013-2015 Laurent Montel <montel@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "newmailnotifierselectcollectionwidget.h"
+#include <AkonadiCore/NewMailNotifierAttribute>
+
+#include <CollectionModifyJob>
+#include <CollectionFilterProxyModel>
+#include <KRecursiveFilterProxyModel>
+
+#include <ChangeRecorder>
+#include <EntityTreeModel>
+#include <Collection>
+#include <KMime/Message>
+
+#include <KCheckableProxyModel>
+
+#include <KLocalizedString>
+#include <QPushButton>
+#include <KLineEdit>
+#include "newmailnotifier_debug.h"
+
+#include <QVBoxLayout>
+#include <QHBoxLayout>
+#include <QTreeView>
+#include <QLabel>
+#include <QTimer>
+
+NewMailNotifierSelectCollectionWidget::NewMailNotifierSelectCollectionWidget(QWidget *parent)
+    : QWidget(parent),
+      mNeedUpdate(false)
+{
+    QVBoxLayout *vbox = new QVBoxLayout;
+
+    QLabel *label = new QLabel(i18n("Select which folders to monitor for new message notifications:"));
+    vbox->addWidget(label);
+
+    // Create a new change recorder.
+    mChangeRecorder = new Akonadi::ChangeRecorder(this);
+    mChangeRecorder->setMimeTypeMonitored(KMime::Message::mimeType());
+    mChangeRecorder->fetchCollection(true);
+    mChangeRecorder->setAllMonitored(true);
+
+    mModel = new Akonadi::EntityTreeModel(mChangeRecorder, this);
+    // Set the model to show only collections, not items.
+    mModel->setItemPopulationStrategy(Akonadi::EntityTreeModel::NoItemPopulation);
+    connect(mModel, &Akonadi::EntityTreeModel::collectionTreeFetched, this, &NewMailNotifierSelectCollectionWidget::slotCollectionTreeFetched);
+
+    Akonadi::CollectionFilterProxyModel *mimeTypeProxy = new Akonadi::CollectionFilterProxyModel(this);
+    mimeTypeProxy->setExcludeVirtualCollections(true);
+    mimeTypeProxy->addMimeTypeFilters(QStringList() << KMime::Message::mimeType());
+    mimeTypeProxy->setSourceModel(mModel);
+
+    // Create the Check proxy model.
+    mSelectionModel = new QItemSelectionModel(mimeTypeProxy);
+    mCheckProxy = new KCheckableProxyModel(this);
+    mCheckProxy->setSelectionModel(mSelectionModel);
+    mCheckProxy->setSourceModel(mimeTypeProxy);
+
+    mCollectionFilter = new KRecursiveFilterProxyModel(this);
+    mCollectionFilter->setSourceModel(mCheckProxy);
+    mCollectionFilter->setDynamicSortFilter(true);
+    mCollectionFilter->setFilterCaseSensitivity(Qt::CaseInsensitive);
+
+    KLineEdit *searchLine = new KLineEdit(this);
+    searchLine->setPlaceholderText(i18n("Search..."));
+    searchLine->setClearButtonShown(true);
+    connect(searchLine, &QLineEdit::textChanged,
+            this, &NewMailNotifierSelectCollectionWidget::slotSetCollectionFilter);
+
+    vbox->addWidget(searchLine);
+
+    mFolderView = new QTreeView;
+    mFolderView->setEditTriggers(QAbstractItemView::NoEditTriggers);
+    mFolderView->setAlternatingRowColors(true);
+    vbox->addWidget(mFolderView);
+
+    mFolderView->setModel(mCollectionFilter);
+
+    QHBoxLayout *hbox = new QHBoxLayout;
+    vbox->addLayout(hbox);
+
+    QPushButton *button = new QPushButton(i18n("&Select All"), this);
+    connect(button, &QPushButton::clicked, this, &NewMailNotifierSelectCollectionWidget::slotSelectAllCollections);
+    hbox->addWidget(button);
+
+    button = new QPushButton(i18n("&Unselect All"), this);
+    connect(button, &QPushButton::clicked, this, &NewMailNotifierSelectCollectionWidget::slotUnselectAllCollections);
+    hbox->addWidget(button);
+    hbox->addStretch(1);
+    setLayout(vbox);
+}
+
+NewMailNotifierSelectCollectionWidget::~NewMailNotifierSelectCollectionWidget()
+{
+
+}
+
+void NewMailNotifierSelectCollectionWidget::slotCollectionTreeFetched()
+{
+    if (!mNeedUpdate) {
+        mNeedUpdate = true;
+        QTimer::singleShot(1000, this, &NewMailNotifierSelectCollectionWidget::slotUpdateCollectionStatus);
+    }
+    mFolderView->expandAll();
+}
+
+void NewMailNotifierSelectCollectionWidget::slotSetCollectionFilter(const QString &filter)
+{
+    mCollectionFilter->setFilterWildcard(filter);
+    mFolderView->expandAll();
+}
+
+void NewMailNotifierSelectCollectionWidget::slotUpdateCollectionStatus()
+{
+    updateStatus(QModelIndex());
+}
+
+void NewMailNotifierSelectCollectionWidget::slotSelectAllCollections()
+{
+    forceStatus(QModelIndex(), true);
+}
+
+void NewMailNotifierSelectCollectionWidget::slotUnselectAllCollections()
+{
+    forceStatus(QModelIndex(), false);
+}
+
+void NewMailNotifierSelectCollectionWidget::updateStatus(const QModelIndex &parent)
+{
+    const int nbCol = mCheckProxy->rowCount(parent);
+    for (int i = 0; i < nbCol; ++i) {
+        const QModelIndex child = mCheckProxy->index(i, 0, parent);
+
+        const Akonadi::Collection collection =
+            mCheckProxy->data(child, Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
+
+        Akonadi::NewMailNotifierAttribute *attr = collection.attribute<Akonadi::NewMailNotifierAttribute>();
+        if (!attr || !attr->ignoreNewMail()) {
+            mCheckProxy->setData(child, Qt::Checked, Qt::CheckStateRole);
+        }
+        updateStatus(child);
+    }
+    mNeedUpdate = false;
+}
+
+void NewMailNotifierSelectCollectionWidget::forceStatus(const QModelIndex &parent, bool status)
+{
+    const int nbCol = mCheckProxy->rowCount(parent);
+    for (int i = 0; i < nbCol; ++i) {
+        const QModelIndex child = mCheckProxy->index(i, 0, parent);
+        mCheckProxy->setData(child, status ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole);
+        forceStatus(child, status);
+    }
+}
+
+void NewMailNotifierSelectCollectionWidget::updateCollectionsRecursive(const QModelIndex &parent)
+{
+    const int nbCol = mCheckProxy->rowCount(parent);
+    for (int i = 0; i < nbCol; ++i) {
+        const QModelIndex child = mCheckProxy->index(i, 0, parent);
+
+        Akonadi::Collection collection =
+            mCheckProxy->data(child, Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
+
+        Akonadi::NewMailNotifierAttribute *attr = collection.attribute<Akonadi::NewMailNotifierAttribute>();
+        Akonadi::CollectionModifyJob *modifyJob = Q_NULLPTR;
+        const bool selected = (mCheckProxy->data(child, Qt::CheckStateRole).value<int>() != 0);
+        if (selected && attr && attr->ignoreNewMail()) {
+            collection.removeAttribute<Akonadi::NewMailNotifierAttribute>();
+            modifyJob = new Akonadi::CollectionModifyJob(collection);
+            modifyJob->setProperty("AttributeAdded", true);
+        } else if (!selected && (!attr || !attr->ignoreNewMail())) {
+            attr = collection.attribute<Akonadi::NewMailNotifierAttribute>(Akonadi::Collection::AddIfMissing);
+            attr->setIgnoreNewMail(true);
+            modifyJob = new Akonadi::CollectionModifyJob(collection);
+            modifyJob->setProperty("AttributeAdded", false);
+        }
+
+        if (modifyJob) {
+            connect(modifyJob, &Akonadi::CollectionModifyJob::finished, this, &NewMailNotifierSelectCollectionWidget::slotModifyJobDone);
+        }
+        updateCollectionsRecursive(child);
+    }
+}
+
+void NewMailNotifierSelectCollectionWidget::slotModifyJobDone(KJob *job)
+{
+    Akonadi::CollectionModifyJob *modifyJob = qobject_cast<Akonadi::CollectionModifyJob *>(job);
+    if (modifyJob && job->error()) {
+        if (job->property("AttributeAdded").toBool()) {
+            qCWarning(NEWMAILNOTIFIER_LOG) << "Failed to append NewMailNotifierAttribute to collection"
+                                           << modifyJob->collection().id() << ":"
+                                           << job->errorString();
+        } else {
+            qCWarning(NEWMAILNOTIFIER_LOG) << "Failed to remove NewMailNotifierAttribute from collection"
+                                           << modifyJob->collection().id() << ":"
+                                           << job->errorString();
+        }
+    }
+}
+
diff --git a/agents/newmailnotifier/newmailnotifierselectcollectionwidget.h b/agents/newmailnotifier/newmailnotifierselectcollectionwidget.h
new file mode 100644 (file)
index 0000000..a45531b
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+    Copyright (c) 2013-2015 Laurent Montel <montel@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef NEWMAILNOTIFIERSELECTCOLLECTIONWIDGET_H
+#define NEWMAILNOTIFIERSELECTCOLLECTIONWIDGET_H
+
+#include <QWidget>
+#include <Collection>
+#include <QModelIndex>
+
+class QItemSelectionModel;
+class KRecursiveFilterProxyModel;
+namespace Akonadi
+{
+class EntityTreeModel;
+class ChangeRecorder;
+}
+class QTreeView;
+class KCheckableProxyModel;
+class KJob;
+
+class NewMailNotifierSelectCollectionWidget : public QWidget
+{
+    Q_OBJECT
+public:
+    explicit NewMailNotifierSelectCollectionWidget(QWidget *parent = Q_NULLPTR);
+    ~NewMailNotifierSelectCollectionWidget();
+
+    void updateCollectionsRecursive(const QModelIndex &parent);
+
+private Q_SLOTS:
+    void slotSelectAllCollections();
+    void slotUnselectAllCollections();
+    void slotModifyJobDone(KJob *job);
+    void slotUpdateCollectionStatus();
+    void slotSetCollectionFilter(const QString &);
+
+    void slotCollectionTreeFetched();
+
+private:
+    void updateStatus(const QModelIndex &parent);
+    void forceStatus(const QModelIndex &parent, bool status);
+    QTreeView *mFolderView;
+    QItemSelectionModel *mSelectionModel;
+    Akonadi::EntityTreeModel *mModel;
+    Akonadi::ChangeRecorder *mChangeRecorder;
+    KCheckableProxyModel *mCheckProxy;
+    KRecursiveFilterProxyModel *mCollectionFilter;
+    bool mNeedUpdate;
+};
+
+#endif // NEWMAILNOTIFIERSELECTCOLLECTIONWIDGET_H
diff --git a/agents/newmailnotifier/newmailnotifiersettingsdialog.cpp b/agents/newmailnotifier/newmailnotifiersettingsdialog.cpp
new file mode 100644 (file)
index 0000000..be057cf
--- /dev/null
@@ -0,0 +1,231 @@
+/*
+    Copyright (c) 2013-2015 Laurent Montel <montel@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "newmailnotifiersettingsdialog.h"
+#include "newmailnotifierattribute.h"
+#include "newmailnotifierselectcollectionwidget.h"
+#include "newmailnotifieragentsettings.h"
+
+#include "kdepim-runtime-version.h"
+
+#include <KLocalizedString>
+#include <KNotifyConfigWidget>
+#include <QLineEdit>
+#include <KCheckableProxyModel>
+#include <QPushButton>
+#include <KHelpMenu>
+#include <kaboutdata.h>
+#include <QIcon>
+
+#include <QTabWidget>
+#include <QCheckBox>
+#include <QGroupBox>
+#include <QVBoxLayout>
+#include <QLabel>
+#include <QWhatsThis>
+#include <QAction>
+
+#include <AkonadiWidgets/CollectionView>
+#include <AkonadiCore/RecursiveCollectionFilterProxyModel>
+#include <AkonadiCore/CollectionFilterProxyModel>
+#include <AkonadiCore/CollectionModifyJob>
+#include <KSharedConfig>
+#include <QDialogButtonBox>
+#include <KConfigGroup>
+
+static const char *textToSpeakMessage =
+    I18N_NOOP("<qt>"
+              "<p>Here you can define message. "
+              "You can use:</p>"
+              "<ul>"
+              "<li>%s set subject</li>"
+              "<li>%f set from</li>"
+              "</ul>"
+              "</qt>");
+
+NewMailNotifierSettingsDialog::NewMailNotifierSettingsDialog(QWidget *parent)
+    : QDialog(parent)
+{
+    setWindowTitle(i18n("New Mail Notifier settings"));
+    setWindowIcon(QIcon::fromTheme(QStringLiteral("kmail")));
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Help);
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    setLayout(mainLayout);
+    QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
+    okButton->setDefault(true);
+    okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
+    connect(buttonBox, &QDialogButtonBox::accepted, this, &NewMailNotifierSettingsDialog::slotOkClicked);
+    connect(buttonBox, &QDialogButtonBox::rejected, this, &NewMailNotifierSettingsDialog::reject);
+
+    QWidget *w = new QWidget;
+    mainLayout->addWidget(w);
+    mainLayout->addWidget(buttonBox);
+    QVBoxLayout *lay = new QVBoxLayout;
+    w->setLayout(lay);
+    QTabWidget *tab = new QTabWidget;
+    lay->addWidget(tab);
+
+    QWidget *settings = new QWidget;
+    QVBoxLayout *vbox = new QVBoxLayout;
+    settings->setLayout(vbox);
+
+    QGroupBox *grp = new QGroupBox(i18n("Choose which fields to show:"));
+    vbox->addWidget(grp);
+    QVBoxLayout *groupboxLayout = new QVBoxLayout;
+    grp->setLayout(groupboxLayout);
+
+    mShowPhoto = new QCheckBox(i18n("Show Photo"));
+    mShowPhoto->setChecked(NewMailNotifierAgentSettings::showPhoto());
+    groupboxLayout->addWidget(mShowPhoto);
+
+    mShowFrom = new QCheckBox(i18n("Show From"));
+    mShowFrom->setChecked(NewMailNotifierAgentSettings::showFrom());
+    groupboxLayout->addWidget(mShowFrom);
+
+    mShowSubject = new QCheckBox(i18n("Show Subject"));
+    mShowSubject->setChecked(NewMailNotifierAgentSettings::showSubject());
+    groupboxLayout->addWidget(mShowSubject);
+
+    mShowFolders = new QCheckBox(i18n("Show Folders"));
+    mShowFolders->setChecked(NewMailNotifierAgentSettings::showFolder());
+    groupboxLayout->addWidget(mShowFolders);
+
+    mExcludeMySelf = new QCheckBox(i18n("Do not notify when email was sent by me"));
+    mExcludeMySelf->setChecked(NewMailNotifierAgentSettings::excludeEmailsFromMe());
+    vbox->addWidget(mExcludeMySelf);
+
+    mAllowToShowMail = new QCheckBox(i18n("Show button to display mail"));
+    mAllowToShowMail->setChecked(NewMailNotifierAgentSettings::showButtonToDisplayMail());
+    vbox->addWidget(mAllowToShowMail);
+
+    vbox->addStretch();
+    tab->addTab(settings, i18n("Display"));
+
+#ifdef HAVE_SPEECH
+    QWidget *textSpeakWidget = new QWidget;
+    vbox = new QVBoxLayout;
+    textSpeakWidget->setLayout(vbox);
+    mTextToSpeak = new QCheckBox(i18n("Enabled"));
+    mTextToSpeak->setChecked(NewMailNotifierAgentSettings::textToSpeakEnabled());
+    vbox->addWidget(mTextToSpeak);
+
+    QLabel *howIsItWork = new QLabel(i18n("<a href=\"whatsthis\">How does this work?</a>"));
+    howIsItWork->setTextInteractionFlags(Qt::LinksAccessibleByMouse);
+    howIsItWork->setContextMenuPolicy(Qt::NoContextMenu);
+    vbox->addWidget(howIsItWork);
+    connect(howIsItWork, &QLabel::linkActivated, this, &NewMailNotifierSettingsDialog::slotHelpLinkClicked);
+
+    QHBoxLayout *textToSpeakLayout = new QHBoxLayout;
+    textToSpeakLayout->setMargin(0);
+    QLabel *lab = new QLabel(i18n("Message:"));
+    textToSpeakLayout->addWidget(lab);
+    mTextToSpeakSetting = new QLineEdit;
+    mTextToSpeakSetting->setClearButtonEnabled(true);
+    mTextToSpeakSetting->setText(NewMailNotifierAgentSettings::textToSpeak());
+    mTextToSpeakSetting->setEnabled(mTextToSpeak->isChecked());
+    mTextToSpeakSetting->setWhatsThis(i18n(textToSpeakMessage));
+    textToSpeakLayout->addWidget(mTextToSpeakSetting);
+    vbox->addLayout(textToSpeakLayout);
+    vbox->addStretch();
+    tab->addTab(textSpeakWidget, i18n("Text to Speak"));
+    connect(mTextToSpeak, &QCheckBox::toggled, mTextToSpeakSetting, &QLineEdit::setEnabled);
+#else
+    mTextToSpeak = Q_NULLPTR;
+    mTextToSpeakSetting = Q_NULLPTR;
+#endif
+
+    mNotify = new KNotifyConfigWidget(this);
+    mNotify->setApplication(QStringLiteral("akonadi_newmailnotifier_agent"));
+    tab->addTab(mNotify, i18n("Notify"));
+
+    mSelectCollection = new NewMailNotifierSelectCollectionWidget;
+    tab->addTab(mSelectCollection, i18n("Folders"));
+
+    KAboutData aboutData = KAboutData(
+                               QStringLiteral("newmailnotifieragent"),
+                               i18n("New Mail Notifier Agent"),
+                               QStringLiteral(KDEPIM_RUNTIME_VERSION),
+                               i18n("Notifies about new mail."),
+                               KAboutLicense::GPL_V2,
+                               i18n("Copyright (C) 2013-2016 Laurent Montel"));
+
+    aboutData.addAuthor(i18n("Laurent Montel"),
+                        i18n("Maintainer"), QStringLiteral("montel@kde.org"));
+    aboutData.setTranslator(i18nc("NAME OF TRANSLATORS", "Your names"),
+                            i18nc("EMAIL OF TRANSLATORS", "Your emails"));
+
+    KHelpMenu *helpMenu = new KHelpMenu(this, aboutData, true);
+    //Initialize menu
+    QMenu *menu = helpMenu->menu();
+    helpMenu->action(KHelpMenu::menuAboutApp)->setIcon(QIcon::fromTheme(QStringLiteral("kmail")));
+    buttonBox->button(QDialogButtonBox::Help)->setMenu(menu);
+    readConfig();
+}
+
+NewMailNotifierSettingsDialog::~NewMailNotifierSettingsDialog()
+{
+    writeConfig();
+}
+
+static const char *myConfigGroupName = "NewMailNotifierDialog";
+
+void NewMailNotifierSettingsDialog::readConfig()
+{
+    KConfigGroup group(KSharedConfig::openConfig(), myConfigGroupName);
+
+    const QSize size = group.readEntry("Size", QSize(500, 300));
+    if (size.isValid()) {
+        resize(size);
+    }
+}
+
+void NewMailNotifierSettingsDialog::writeConfig()
+{
+    KConfigGroup group(KSharedConfig::openConfig(), myConfigGroupName);
+    group.writeEntry("Size", size());
+    group.sync();
+}
+
+void NewMailNotifierSettingsDialog::slotHelpLinkClicked(const QString &)
+{
+    const QString help =
+        i18n(textToSpeakMessage);
+
+    QWhatsThis::showText(QCursor::pos(), help);
+}
+
+void NewMailNotifierSettingsDialog::slotOkClicked()
+{
+    mSelectCollection->updateCollectionsRecursive(QModelIndex());
+
+    NewMailNotifierAgentSettings::setShowPhoto(mShowPhoto->isChecked());
+    NewMailNotifierAgentSettings::setShowFrom(mShowFrom->isChecked());
+    NewMailNotifierAgentSettings::setShowSubject(mShowSubject->isChecked());
+    NewMailNotifierAgentSettings::setShowFolder(mShowFolders->isChecked());
+    NewMailNotifierAgentSettings::setExcludeEmailsFromMe(mExcludeMySelf->isChecked());
+#ifdef HAVE_SPEECH
+    NewMailNotifierAgentSettings::setTextToSpeakEnabled(mTextToSpeak->isChecked());
+    NewMailNotifierAgentSettings::setTextToSpeak(mTextToSpeakSetting->text());
+#endif
+    NewMailNotifierAgentSettings::setShowButtonToDisplayMail(mAllowToShowMail->isChecked());
+    NewMailNotifierAgentSettings::self()->save();
+    mNotify->save();
+    accept();
+}
+
diff --git a/agents/newmailnotifier/newmailnotifiersettingsdialog.h b/agents/newmailnotifier/newmailnotifiersettingsdialog.h
new file mode 100644 (file)
index 0000000..dba5cdb
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+    Copyright (c) 2013-2015 Laurent Montel <montel@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef NEWMAILNOTIFIERSETTINGSDIALOG_H
+#define NEWMAILNOTIFIERSETTINGSDIALOG_H
+
+#include <QDialog>
+#include <Collection>
+
+class KNotifyConfigWidget;
+class QCheckBox;
+class QLineEdit;
+class NewMailNotifierSelectCollectionWidget;
+class NewMailNotifierSettingsDialog : public QDialog
+{
+    Q_OBJECT
+public:
+    explicit NewMailNotifierSettingsDialog(QWidget *parent = Q_NULLPTR);
+    ~NewMailNotifierSettingsDialog();
+
+private Q_SLOTS:
+    void slotOkClicked();
+    void slotHelpLinkClicked(const QString &);
+
+private:
+    void writeConfig();
+    void readConfig();
+    QCheckBox *mShowPhoto;
+    QCheckBox *mShowFrom;
+    QCheckBox *mShowSubject;
+    QCheckBox *mShowFolders;
+    QCheckBox *mExcludeMySelf;
+    QCheckBox *mAllowToShowMail;
+    KNotifyConfigWidget *mNotify;
+    QCheckBox *mTextToSpeak;
+    QLineEdit *mTextToSpeakSetting;
+    NewMailNotifierSelectCollectionWidget *mSelectCollection;
+};
+
+#endif // NEWMAILNOTIFIERSETTINGSDIALOG_H
diff --git a/agents/newmailnotifier/newmailnotifiershowmessagejob.cpp b/agents/newmailnotifier/newmailnotifiershowmessagejob.cpp
new file mode 100644 (file)
index 0000000..44c25e4
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+  Copyright (c) 2014 Montel Laurent <montel@kde.org>
+
+  This program is free software; you can redistribute it and/or modify it
+  under the terms of the GNU General Public License, version 2, as
+  published by the Free Software Foundation.
+
+  This program is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#include "newmailnotifiershowmessagejob.h"
+#include "newmailnotifier_debug.h"
+#include <QDBusConnection>
+#include <QDBusInterface>
+#include <QDBusReply>
+#include <QDBusConnectionInterface>
+#include <ktoolinvocation.h>
+
+NewMailNotifierShowMessageJob::NewMailNotifierShowMessageJob(Akonadi::Item::Id id, QObject *parent)
+    : KJob(parent),
+      mId(id)
+{
+}
+
+NewMailNotifierShowMessageJob::~NewMailNotifierShowMessageJob()
+{
+}
+
+void NewMailNotifierShowMessageJob::start()
+{
+    if (mId < 0) {
+        Q_EMIT emitResult();
+        return;
+    }
+    const QString kmailInterface = QStringLiteral("org.kde.kmail");
+    QDBusReply<bool> reply = QDBusConnection::sessionBus().interface()->isServiceRegistered(kmailInterface);
+    if (!reply.isValid() || !reply.value()) {
+        // Program is not already running, so start it
+        QString errmsg;
+        if (KToolInvocation::startServiceByDesktopName(QStringLiteral("kmail2"), QString(), &errmsg)) {
+            qCDebug(NEWMAILNOTIFIER_LOG) << " Can not start kmail" << errmsg;
+            setError(UserDefinedError);
+            Q_EMIT emitResult();
+            return;
+        }
+    }
+    QDBusInterface kmail(kmailInterface, QStringLiteral("/KMail"), QStringLiteral("org.kde.kmail.kmail"));
+    if (kmail.isValid()) {
+        kmail.call(QStringLiteral("showMail"), mId);
+    }
+    Q_EMIT emitResult();
+}
diff --git a/agents/newmailnotifier/newmailnotifiershowmessagejob.h b/agents/newmailnotifier/newmailnotifiershowmessagejob.h
new file mode 100644 (file)
index 0000000..2ae0b82
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+  Copyright (c) 2014 Montel Laurent <montel@kde.org>
+
+  This program is free software; you can redistribute it and/or modify it
+  under the terms of the GNU General Public License, version 2, as
+  published by the Free Software Foundation.
+
+  This program is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#ifndef NEWMAILNOTIFIERSHOWMESSAGEJOB_H
+#define NEWMAILNOTIFIERSHOWMESSAGEJOB_H
+
+#include <KJob>
+#include <AkonadiCore/Item>
+
+class NewMailNotifierShowMessageJob : public KJob
+{
+    Q_OBJECT
+public:
+    explicit NewMailNotifierShowMessageJob(Akonadi::Item::Id id, QObject *parent = Q_NULLPTR);
+    ~NewMailNotifierShowMessageJob();
+
+    void start() Q_DECL_OVERRIDE;
+
+private:
+    Akonadi::Item::Id mId;
+
+};
+
+#endif // NEWMAILNOTIFIERSHOWMESSAGEJOB_H
diff --git a/agents/newmailnotifier/org.freedesktop.Akonadi.NewMailNotifier.xml b/agents/newmailnotifier/org.freedesktop.Akonadi.NewMailNotifier.xml
new file mode 100644 (file)
index 0000000..c736132
--- /dev/null
@@ -0,0 +1,71 @@
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<node>
+  <interface name="org.freedesktop.Akonadi.NewMailNotifier">
+    <method name="showConfigureDialog" >
+      <arg direction="in" type="x" name="windowId" />
+    </method>
+    <method name="setEnableAgent" >
+      <arg type="b" direction="in"/>
+    </method>
+    <method name="enabledAgent" >
+      <arg type="b" direction="out"/>
+    </method>
+    <method name="setVerboseMailNotification" >
+      <arg type="b" direction="in"/>
+    </method>
+    <method name="verboseMailNotification" >
+      <arg type="b" direction="out"/>
+    </method>
+    <method name="setShowPhoto" >
+      <arg type="b" direction="in"/>
+    </method>
+    <method name="showPhoto" >
+      <arg type="b" direction="out"/>
+    </method>
+    <method name="setShowFrom" >
+      <arg type="b" direction="in"/>
+    </method>
+    <method name="showFrom" >
+      <arg type="b" direction="out"/>
+    </method>
+    <method name="setShowSubject" >
+      <arg type="b" direction="in"/>
+    </method>
+    <method name="showSubject" >
+      <arg type="b" direction="out"/>
+    </method>
+    <method name="setShowFolderName" >
+      <arg type="b" direction="in"/>
+    </method>
+    <method name="showFolderName" >
+      <arg type="b" direction="out"/>
+    </method>
+    <method name="setExcludeMyselfFromNotification" >
+      <arg type="b" direction="in"/>
+    </method>
+    <method name="excludeMyselfFromNotification" >
+      <arg type="b" direction="out"/>
+    </method>
+    <method name="setTextToSpeakEnabled" >
+      <arg type="b" direction="in"/>
+    </method>
+    <method name="textToSpeakEnabled" >
+      <arg type="b" direction="out"/>
+    </method>
+    <method name="setTextToSpeak" >
+      <arg type="s" direction="in"/>
+    </method>
+    <method name="textToSpeak" >
+      <arg type="s" direction="out"/>
+    </method>
+    <method name="setShowButtonToDisplayMail" >
+      <arg type="b" direction="in"/>
+    </method>
+    <method name="showButtonToDisplayMail" >
+      <arg type="b" direction="out"/>
+    </method>
+
+
+    <method name="printDebug" />
+  </interface>
+</node>
diff --git a/agents/newmailnotifier/specialnotifierjob.cpp b/agents/newmailnotifier/specialnotifierjob.cpp
new file mode 100644 (file)
index 0000000..349f250
--- /dev/null
@@ -0,0 +1,181 @@
+/*
+  Copyright (c) 2013-2016 Montel Laurent <montel@kde.org>
+
+  This program is free software; you can redistribute it and/or modify it
+  under the terms of the GNU General Public License, version 2, as
+  published by the Free Software Foundation.
+
+  This program is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#include "newmailnotifiershowmessagejob.h"
+#include "specialnotifierjob.h"
+#include "newmailnotifieragentsettings.h"
+
+#include <Akonadi/Contact/ContactSearchJob>
+#include <ItemFetchJob>
+#include <ItemFetchScope>
+#include <Akonadi/KMime/MessageParts>
+
+#include <KNotification>
+#include <KEmailAddress>
+
+#include <KMime/Message>
+
+#include <KLocalizedString>
+#include "newmailnotifier_debug.h"
+
+#include <QTextDocument>
+#ifdef HAVE_SPEECH
+#include <QTextToSpeech>
+#endif
+
+SpecialNotifierJob::SpecialNotifierJob(const QStringList &listEmails, const QString &path, Akonadi::Item::Id id, QObject *parent)
+    : QObject(parent),
+      mListEmails(listEmails),
+      mPath(path),
+      mItemId(id)
+{
+    Akonadi::Item item(mItemId);
+    Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob(item, this);
+    job->fetchScope().fetchPayloadPart(Akonadi::MessagePart::Envelope, true);
+
+    connect(job, &Akonadi::ItemFetchJob::result, this, &SpecialNotifierJob::slotItemFetchJobDone);
+}
+
+SpecialNotifierJob::~SpecialNotifierJob()
+{
+
+}
+
+void SpecialNotifierJob::setDefaultPixmap(const QPixmap &pixmap)
+{
+    mDefaultPixmap = pixmap;
+}
+
+void SpecialNotifierJob::slotItemFetchJobDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(NEWMAILNOTIFIER_LOG) << job->errorString();
+        deleteLater();
+        return;
+    }
+
+    const Akonadi::Item::List lst = qobject_cast<Akonadi::ItemFetchJob *>(job)->items();
+    if (lst.count() == 1) {
+        const Akonadi::Item item = lst.first();
+        if (!item.hasPayload<KMime::Message::Ptr>()) {
+            qCDebug(NEWMAILNOTIFIER_LOG) << " message has not payload.";
+            deleteLater();
+            return;
+        }
+
+        const KMime::Message::Ptr mb = item.payload<KMime::Message::Ptr>();
+        mFrom = mb->from()->asUnicodeString();
+        mSubject = mb->subject()->asUnicodeString();
+        if (NewMailNotifierAgentSettings::showPhoto()) {
+            Akonadi::ContactSearchJob *job = new Akonadi::ContactSearchJob(this);
+            job->setLimit(1);
+            job->setQuery(Akonadi::ContactSearchJob::Email, KEmailAddress::firstEmailAddress(mFrom).toLower(), Akonadi::ContactSearchJob::ExactMatch);
+            connect(job, &Akonadi::ItemFetchJob::result, this, &SpecialNotifierJob::slotSearchJobFinished);
+        } else {
+            emitNotification(mDefaultPixmap);
+            deleteLater();
+        }
+    } else {
+        qCWarning(NEWMAILNOTIFIER_LOG) << " Found item different from 1: " << lst.count();
+        deleteLater();
+        return;
+    }
+}
+
+void SpecialNotifierJob::slotSearchJobFinished(KJob *job)
+{
+    const Akonadi::ContactSearchJob *searchJob = qobject_cast<Akonadi::ContactSearchJob *>(job);
+    if (searchJob->error()) {
+        qCWarning(NEWMAILNOTIFIER_LOG) << "Unable to fetch contact:" << searchJob->errorText();
+        emitNotification(mDefaultPixmap);
+        return;
+    }
+    if (!searchJob->contacts().isEmpty()) {
+        const KContacts::Addressee addressee = searchJob->contacts().at(0);
+        const KContacts::Picture photo = addressee.photo();
+        const QImage image = photo.data();
+        if (image.isNull()) {
+            emitNotification(mDefaultPixmap);
+        } else {
+            emitNotification(QPixmap::fromImage(image));
+        }
+    } else {
+        emitNotification(mDefaultPixmap);
+    }
+}
+
+void SpecialNotifierJob::emitNotification(const QPixmap &pixmap)
+{
+    if (NewMailNotifierAgentSettings::excludeEmailsFromMe()) {
+        Q_FOREACH (const QString &email, mListEmails) {
+            if (mFrom.contains(email)) {
+                //Exclude this notification
+                deleteLater();
+                return;
+            }
+        }
+    }
+
+    QStringList result;
+    if (NewMailNotifierAgentSettings::showFrom()) {
+        result << i18n("From: %1", mFrom.toHtmlEscaped());
+    }
+    if (NewMailNotifierAgentSettings::showSubject()) {
+        QString subject = mSubject.simplified();
+        if (subject.length() > 80) {
+            subject.truncate(80);
+            subject += QStringLiteral("...");
+        }
+        result << i18n("Subject: %1", subject.toHtmlEscaped());
+    }
+    if (NewMailNotifierAgentSettings::showFolder()) {
+        result << i18n("In: %1", mPath);
+    }
+
+    if (NewMailNotifierAgentSettings::textToSpeakEnabled()) {
+        if (!NewMailNotifierAgentSettings::textToSpeak().isEmpty()) {
+#ifdef HAVE_SPEECH
+            QTextToSpeech *speech = new QTextToSpeech(this);
+            QString message = NewMailNotifierAgentSettings::textToSpeak();
+            message.replace(QStringLiteral("%s"), mSubject.toHtmlEscaped());
+            message.replace(QStringLiteral("%f"), mFrom.toHtmlEscaped());
+            speech->say(message);
+#endif
+        }
+    }
+
+    if (NewMailNotifierAgentSettings::showButtonToDisplayMail()) {
+        KNotification *notification = new KNotification(QStringLiteral("new-email"), Q_NULLPTR, KNotification::CloseOnTimeout);
+        notification->setText(result.join(QStringLiteral("\n")));
+        notification->setPixmap(pixmap);
+        notification->setActions(QStringList() << i18n("Show mail..."));
+
+        connect(notification, static_cast<void (KNotification::*)(unsigned int)>(&KNotification::activated), this, &SpecialNotifierJob::slotOpenMail);
+        connect(notification, &KNotification::closed, this, &SpecialNotifierJob::deleteLater);
+
+        notification->sendEvent();
+    } else {
+        Q_EMIT displayNotification(pixmap, result.join(QStringLiteral("\n")));
+        deleteLater();
+    }
+}
+
+void SpecialNotifierJob::slotOpenMail()
+{
+    NewMailNotifierShowMessageJob *job = new NewMailNotifierShowMessageJob(mItemId);
+    job->start();
+}
diff --git a/agents/newmailnotifier/specialnotifierjob.h b/agents/newmailnotifier/specialnotifierjob.h
new file mode 100644 (file)
index 0000000..ddd465d
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+  Copyright (c) 2013-2016 Montel Laurent <montel@kde.org>
+
+  This program is free software; you can redistribute it and/or modify it
+  under the terms of the GNU General Public License, version 2, as
+  published by the Free Software Foundation.
+
+  This program is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#ifndef SPECIALNOTIFIERJOB_H
+#define SPECIALNOTIFIERJOB_H
+
+#include <QObject>
+#include <Item>
+#include <QStringList>
+#include <QPixmap>
+class KJob;
+
+class SpecialNotifierJob : public QObject
+{
+    Q_OBJECT
+public:
+    explicit SpecialNotifierJob(const QStringList &listEmails, const QString &path, Akonadi::Item::Id id, QObject *parent = Q_NULLPTR);
+    ~SpecialNotifierJob();
+
+    void setDefaultPixmap(const QPixmap &pixmap);
+
+Q_SIGNALS:
+    void displayNotification(const QPixmap &pixmap, const QString &message);
+
+private Q_SLOTS:
+    void slotSearchJobFinished(KJob *job);
+    void slotItemFetchJobDone(KJob *);
+    void slotOpenMail();
+private:
+    void emitNotification(const QPixmap &pixmap);
+    QPixmap mDefaultPixmap;
+    QStringList mListEmails;
+    QString mSubject;
+    QString mFrom;
+    QString mPath;
+    Akonadi::Item::Id mItemId;
+};
+
+#endif // SPECIALNOTIFIERJOB_H
diff --git a/akonadi-prefix.h.cmake b/akonadi-prefix.h.cmake
new file mode 100644 (file)
index 0000000..b2753f1
--- /dev/null
@@ -0,0 +1,6 @@
+/* This file contains all the paths that change when changing the installation prefix */
+
+#define AKONADIPREFIX "${CMAKE_INSTALL_PREFIX}"
+#define AKONADIDATA   "${SHARE_INSTALL_PREFIX}"
+#define AKONADICONFIG "${CONFIG_INSTALL_DIR}"
+
diff --git a/akonadi-version.h.cmake b/akonadi-version.h.cmake
new file mode 100644 (file)
index 0000000..58c028e
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+    This file is part of kdepim.
+    Copyright (C) 2009 Christophe Giboudeaux  <cgiboudeaux@gmail.com>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#ifndef AKONADI_VERSION_H
+#define AKONADI_VERSION_H
+
+#define AKONADI_VERSION "@AKONADI_VERSION@"
+
+#endif // AKONADI_VERSION_H
\ No newline at end of file
diff --git a/cmake/modules/FindXsltproc.cmake b/cmake/modules/FindXsltproc.cmake
new file mode 100644 (file)
index 0000000..45b46cf
--- /dev/null
@@ -0,0 +1,32 @@
+# Find xsltproc executable and provide a macro to generate D-Bus interfaces.
+#
+# The following variables are defined :
+# XSLTPROC_EXECUTABLE - path to the xsltproc executable
+# Xsltproc_FOUND - true if the program was found
+#
+find_program(XSLTPROC_EXECUTABLE xsltproc DOC "Path to the xsltproc executable")
+mark_as_advanced(XSLTPROC_EXECUTABLE)
+
+if(XSLTPROC_EXECUTABLE)
+  set(Xsltproc_FOUND TRUE)
+
+  # We depend on kdepimlibs, make sure it's found
+  if(NOT DEFINED KF5Akonadi_DATA_DIR)
+    find_package(KF5Akonadi REQUIRED)
+  endif()
+
+
+  # Macro to generate a D-Bus interface description from a KConfigXT file
+  macro(kcfg_generate_dbus_interface _kcfg _name)
+    add_custom_command(
+      OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${_name}.xml
+      COMMAND ${XSLTPROC_EXECUTABLE} --stringparam interfaceName ${_name}
+      ${KF5Akonadi_DATA_DIR}/kcfg2dbus.xsl
+      ${_kcfg}
+      > ${CMAKE_CURRENT_BINARY_DIR}/${_name}.xml
+      DEPENDS ${KF5Akonadi_DATA_DIR}/kcfg2dbus.xsl
+      ${_kcfg}
+      )
+  endmacro()
+endif()
+
diff --git a/defaultsetup/CMakeLists.txt b/defaultsetup/CMakeLists.txt
new file mode 100644 (file)
index 0000000..c0394b8
--- /dev/null
@@ -0,0 +1,10 @@
+configure_file(defaultaddressbook.desktop ${CMAKE_CURRENT_BINARY_DIR}/defaultaddressbook)
+configure_file(defaultcalendar.desktop ${CMAKE_CURRENT_BINARY_DIR}/defaultcalendar)
+configure_file(defaultnotebook.desktop ${CMAKE_CURRENT_BINARY_DIR}/defaultnotebook)
+configure_file(birthdaycalendar.desktop ${CMAKE_CURRENT_BINARY_DIR}/birthdaycalendar)
+
+install ( FILES ${CMAKE_CURRENT_BINARY_DIR}/defaultcalendar
+                ${CMAKE_CURRENT_BINARY_DIR}/defaultaddressbook
+                ${CMAKE_CURRENT_BINARY_DIR}/defaultnotebook
+                ${CMAKE_CURRENT_BINARY_DIR}/birthdaycalendar
+                DESTINATION ${KDE_INSTALL_DATADIR}/akonadi/firstrun )
diff --git a/defaultsetup/birthdaycalendar.desktop b/defaultsetup/birthdaycalendar.desktop
new file mode 100644 (file)
index 0000000..53aa1e8
--- /dev/null
@@ -0,0 +1,3 @@
+[Agent]
+Id=birthdaycalendar
+Type=akonadi_birthdays_resource
diff --git a/defaultsetup/defaultaddressbook.desktop b/defaultsetup/defaultaddressbook.desktop
new file mode 100644 (file)
index 0000000..cb01013
--- /dev/null
@@ -0,0 +1,56 @@
+[Agent]
+Id=defaultaddressbook
+Type=akonadi_contacts_resource
+Name=Personal Contacts
+Name[ast]=Contautos personales
+Name[bg]=Лични контакти
+Name[bs]=Lični kontakti
+Name[ca]=Contactes personals
+Name[ca@valencia]=Contactes personals
+Name[cs]=Osobní kontakty
+Name[da]=Personlige kontakter
+Name[de]=Persönliche Kontakte
+Name[el]=Προσωπικές επαφές
+Name[en_GB]=Personal Contacts
+Name[es]=Contactos personales
+Name[et]=Isiklikud kontaktid
+Name[fi]=Omat yhteystiedot
+Name[fr]=Contacts personnels
+Name[ga]=Teagmhálacha Pearsanta
+Name[gl]=Contactos Persoais
+Name[hu]=Személyes névjegyek
+Name[ia]=Contactos personal
+Name[it]=Contatti personali
+Name[ja]=個人の連絡先
+Name[kk]=Дербес контакттар
+Name[km]=ទំនាក់ទំនង​ផ្ទាល់ខ្លួន
+Name[ko]=개인 연락처
+Name[lt]=Asmeniniai kontaktai
+Name[lv]=Personīgie kontakti
+Name[nb]=Personlige kontakter
+Name[nds]=Persöönlich Kontakten
+Name[nl]=Persoonlijke contacten
+Name[nn]=Personlege kontaktar
+Name[pa]=ਨਿੱਜੀ ਸੰਪਰਕ
+Name[pl]=Kontakty osobiste
+Name[pt]=Contactos Pessoais
+Name[pt_BR]=Contatos pessoais
+Name[ro]=Contacte personale
+Name[ru]=Личные контакты
+Name[sk]=Osobné kontakty
+Name[sl]=Osebni stiki
+Name[sr]=Лични контакти
+Name[sr@ijekavian]=Лични контакти
+Name[sr@ijekavianlatin]=Lični kontakti
+Name[sr@latin]=Lični kontakti
+Name[sv]=Personliga kontakter
+Name[tr]=Kişisel Bağlantılar
+Name[ug]=شەخسىي ئالاقەداشلار
+Name[uk]=Особисті контакти
+Name[x-test]=xxPersonal Contactsxx
+Name[zh_CN]=个人联系人
+Name[zh_TW]=個人聯絡人
+
+[Settings]
+IsConfigured=true
+Path[$e]=$HOME/.local/share/contacts/
diff --git a/defaultsetup/defaultcalendar.desktop b/defaultsetup/defaultcalendar.desktop
new file mode 100644 (file)
index 0000000..2b8c8c9
--- /dev/null
@@ -0,0 +1,52 @@
+[Agent]
+Id=defaultcalendar
+Type=akonadi_ical_resource
+Name=Personal Calendar
+Name[ast]=Calendariu personal
+Name[bg]=Личен календар
+Name[bs]=Lični kalendar
+Name[ca]=Calendari personal
+Name[ca@valencia]=Calendari personal
+Name[cs]=Osobní kalendář
+Name[da]=Personlig kalender
+Name[de]=Persönlicher Kalender
+Name[el]=Προσωπικό ημερολόγιο
+Name[en_GB]=Personal Calendar
+Name[es]=Calendario personal
+Name[et]=Isiklik kalender
+Name[fi]=Oma kalenteri
+Name[fr]=Agenda personnel
+Name[ga]=Féilire Pearsanta
+Name[gl]=Calendario persoal
+Name[hu]=Személyes naptár
+Name[ia]=Calendario Personal
+Name[it]=Calendario personale
+Name[kk]=Дербес күнтізбе
+Name[km]=ប្រតិទិន​ផ្ទាល់ខ្លួន
+Name[ko]=개인 달력
+Name[lt]=Asmeninis kalendorius
+Name[lv]=Personīgais kalendārs
+Name[nb]=Personlig kalender
+Name[nds]=Persöönlich Kalenner
+Name[nl]=Persoonlijke agenda
+Name[pl]=Kalendarz osobisty
+Name[pt]=Calendário Pessoal
+Name[pt_BR]=Calendário pessoal
+Name[ro]=Calendar personal
+Name[ru]=Личный календарь
+Name[sk]=Osobný kalendár
+Name[sl]=Osebni koledar
+Name[sr]=Лични календар
+Name[sr@ijekavian]=Лични календар
+Name[sr@ijekavianlatin]=Lični kalendar
+Name[sr@latin]=Lični kalendar
+Name[sv]=Personlig kalender
+Name[tr]=Kişisel Takvim
+Name[uk]=Особистий календар
+Name[x-test]=xxPersonal Calendarxx
+Name[zh_CN]=个人日历
+Name[zh_TW]=個人行事曆
+
+[Settings]
+Path[$e]=$HOME/.local/share/apps/korganizer/std.ics
+
diff --git a/defaultsetup/defaultnotebook.desktop b/defaultsetup/defaultnotebook.desktop
new file mode 100644 (file)
index 0000000..753431a
--- /dev/null
@@ -0,0 +1,79 @@
+[Agent]
+Id=defaultnotebook
+Type=akonadi_akonotes_resource
+Name=Notes
+Name[af]=Notas
+Name[ar]=ملاحظات
+Name[ast]=Notes
+Name[be]=Заметкі
+Name[bg]=Бележки
+Name[br]=Notennoù
+Name[bs]=Bilješke
+Name[ca]=Notes
+Name[ca@valencia]=Notes
+Name[cs]=Poznámky
+Name[cy]=Nodiadau
+Name[da]=Noter
+Name[de]=Notizen
+Name[el]=Σημειώσεις
+Name[en_GB]=Notes
+Name[eo]=Notoj
+Name[es]=Notas
+Name[et]=Sedelid
+Name[eu]=Oharrak
+Name[fa]=یادداشتها
+Name[fi]=Muistiinpanot
+Name[fr]=Notes
+Name[fy]=Notysjes
+Name[ga]=Nótaí
+Name[gl]=Notas
+Name[he]=פתקים
+Name[hu]=Feljegyzések
+Name[ia]=Notas
+Name[is]=Minnismiðar
+Name[it]=Note
+Name[ja]=メモ
+Name[ka]=ჩანიშვნები
+Name[kk]=Жазбалар
+Name[km]=ចំណាំ
+Name[ko]=노트
+Name[lt]=Užrašai
+Name[lv]=Piezīmes
+Name[mai]=टिप्पणी
+Name[mk]=Белешки
+Name[ms]=Nota
+Name[nb]=Notater
+Name[nds]=Notizen
+Name[ne]=टिपोट
+Name[nl]=Notities
+Name[nn]=Notat
+Name[pa]=ਨੋਟਿਸ
+Name[pl]=Notatki
+Name[pt]=Notas
+Name[pt_BR]=Notas
+Name[ro]=Notițe
+Name[ru]=Заметки
+Name[se]=Nohtat
+Name[sk]=Poznámky
+Name[sl]=Sporočilca
+Name[sq]=Shënimet
+Name[sr]=Белешке
+Name[sr@ijekavian]=Биљешке
+Name[sr@ijekavianlatin]=Bilješke
+Name[sr@latin]=Beleške
+Name[sv]=Anteckningar
+Name[ta]=குறிப்புகள்
+Name[tg]=Ахборот
+Name[th]=บันทึกย่อ
+Name[tr]=Notlar
+Name[ug]=ئىزاھ
+Name[uk]=Примітки
+Name[uz]=Yozma xotira
+Name[uz@cyrillic]=Ёзма хотира
+Name[wa]=Notes
+Name[x-test]=xxNotesxx
+Name[zh_CN]=便笺
+Name[zh_TW]=備忘錄
+
+[Settings]
+Path[$e]=$HOME/.local/share/notes/
diff --git a/doc/git-migration.txt b/doc/git-migration.txt
new file mode 100644 (file)
index 0000000..2908dd6
--- /dev/null
@@ -0,0 +1,7 @@
+Migrated to git using the following:
+
+svn2git: http://gitorious.org/svn2git version: 409d8bc4cbaade82672f251c45178c3cfed4619d
+kde-ruleset: http://projects.kde.org/projects/playground/sdk/kde-ruleset/ version: d71b11d9434d951bdaaff0f7cd3de9dcae000018
+kde svn repo synced at revision: 1208541
+
+command: time ionice -c3 nice svn-all-fast-export --identity-map ../kde-ruleset/account-map --rules ../kde-ruleset/kdepim-runtime-rules --add-metadata --debug-rules --stats --svn-branches ../svn/
diff --git a/doc/libakonadi.xmi b/doc/libakonadi.xmi
new file mode 100644 (file)
index 0000000..72f35f3
--- /dev/null
@@ -0,0 +1,9639 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<XMI xmlns:UML="http://schema.omg.org/spec/UML/1.3" verified="false" timestamp="2006-07-05T17:49:33" xmi.version="1.2" >
+ <XMI.header>
+  <XMI.documentation>
+   <XMI.exporter>umbrello uml modeller http://uml.sf.net</XMI.exporter>
+   <XMI.exporterVersion>1.5.3</XMI.exporterVersion>
+   <XMI.exporterEncoding>UnicodeUTF8</XMI.exporterEncoding>
+  </XMI.documentation>
+  <XMI.metamodel xmi.name="UML" href="UML.xml" xmi.version="1.3" />
+ </XMI.header>
+ <XMI.content>
+  <UML:Model isSpecification="false" isLeaf="false" isRoot="false" xmi.id="m1" isAbstract="false" name="UML-Modell" >
+   <UML:Namespace.ownedElement>
+    <UML:Stereotype isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="131" isRoot="false" isAbstract="false" name="datatype" />
+    <UML:Stereotype isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="142" isRoot="false" isAbstract="false" name="constructor" />
+    <UML:Stereotype isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="143" isRoot="false" isAbstract="false" name="friend" />
+    <UML:Stereotype isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="144" isRoot="false" isAbstract="false" name="virtual" />
+    <UML:DataType stereotype="131" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="130" isRoot="false" isAbstract="false" name="int" />
+    <UML:DataType stereotype="131" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="132" isRoot="false" isAbstract="false" name="char" />
+    <UML:DataType stereotype="131" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="133" isRoot="false" isAbstract="false" name="bool" />
+    <UML:DataType stereotype="131" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="134" isRoot="false" isAbstract="false" name="float" />
+    <UML:DataType stereotype="131" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="135" isRoot="false" isAbstract="false" name="double" />
+    <UML:DataType stereotype="131" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="136" isRoot="false" isAbstract="false" name="short" />
+    <UML:DataType stereotype="131" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="137" isRoot="false" isAbstract="false" name="long" />
+    <UML:DataType stereotype="131" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="138" isRoot="false" isAbstract="false" name="unsigned int" />
+    <UML:DataType stereotype="131" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="139" isRoot="false" isAbstract="false" name="unsigned short" />
+    <UML:DataType stereotype="131" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="140" isRoot="false" isAbstract="false" name="unsigned long" />
+    <UML:DataType stereotype="131" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="141" isRoot="false" isAbstract="false" name="string" />
+    <UML:Class comment="Representation of an email message or a news article." isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="145" isRoot="false" isAbstract="false" name="Message" >
+     <UML:GeneralizableElement.generalization>
+      <UML:Generalization xmi.idref="163" />
+     </UML:GeneralizableElement.generalization>
+    </UML:Class>
+    <UML:Class comment=" A flat self-updating message model.
+" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="146" isRoot="false" isAbstract="false" name="MessageModel" />
+    <UML:Class comment="Fetches message data from the backend." isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="147" isRoot="false" isAbstract="false" name="MessageFetchJob" >
+     <UML:GeneralizableElement.generalization>
+      <UML:Generalization xmi.idref="159" />
+      <UML:Generalization xmi.idref="166" />
+     </UML:GeneralizableElement.generalization>
+    </UML:Class>
+    <UML:Class comment="Retrieving of (partial) messages matching a given query pattern." isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="148" isRoot="false" isAbstract="false" name="MessageQuery" >
+     <UML:GeneralizableElement.generalization>
+      <UML:Generalization xmi.idref="195" />
+     </UML:GeneralizableElement.generalization>
+    </UML:Class>
+    <UML:Class comment="Base class for all PIM items stored in Akonadi.
+  It contains type-neutral data and the unique reference.
+" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="150" isRoot="false" isAbstract="false" name="Item" />
+    <UML:Class comment="  This class encapsulates a request to the pim storage service,
+  the code looks like
+
+  \code
+    PIM::Job *job = new PIM::SomeJob( some parameter );
+    connect( job, SIGNAL( done( PIM::Job* ) ),
+             this, SLOT( slotResult( PIM::Job* ) ) );
+    job->start();
+  \endcode
+
+  And the slotResult is usually at least:
+
+  \code
+    if ( job->error() )
+      // handle error...
+  \endcode
+
+  With the synchronous interface the code looks like
+
+  \code
+    PIM::SomeJob job( some parameter );
+    if ( !job.exec() ) {
+      qDebug( &quot;Error: %s&quot;, qPrintable( job.errorString() ) );
+    } else {
+      // do something
+    }
+  \endcode
+
+  Subclasses must reimplement @see doStart().
+" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="155" isRoot="false" isAbstract="false" name="Job" />
+    <UML:Class comment=" This class represents a collection of PIM objects, such as a folder on a mail- or
+  groupware-server.
+
+  Collections are hierarchical, i.e. they may have a parent collectio" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="169" isRoot="false" isAbstract="false" name="Collection" />
+    <UML:Class isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="170" isRoot="false" isAbstract="false" name="CollectionAttribute" />
+    <UML:Class comment="Job to create collections." isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="179" isRoot="false" isAbstract="false" name="CollectionCreateJob" >
+     <UML:GeneralizableElement.generalization>
+      <UML:Generalization xmi.idref="180" />
+     </UML:GeneralizableElement.generalization>
+    </UML:Class>
+    <UML:Class comment="Job to rename/move a collection." isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="183" isRoot="false" isAbstract="false" name="CollectionRenameJob" >
+     <UML:GeneralizableElement.generalization>
+      <UML:Generalization xmi.idref="184" />
+     </UML:GeneralizableElement.generalization>
+    </UML:Class>
+    <UML:Class comment="Selects a specific collection. See RFC 3501 for select semantics." isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="187" isRoot="false" isAbstract="false" name="CollectionSelectJob" >
+     <UML:GeneralizableElement.generalization>
+      <UML:Generalization xmi.idref="188" />
+     </UML:GeneralizableElement.generalization>
+    </UML:Class>
+    <UML:Class comment="Creates a new PIM item on the backend.
+Can be used as base class for type specific append jobs.
+" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="191" isRoot="false" isAbstract="false" name="ItemAppendJob" >
+     <UML:GeneralizableElement.generalization>
+      <UML:Generalization xmi.idref="192" />
+     </UML:GeneralizableElement.generalization>
+    </UML:Class>
+    <UML:Class comment=" This class requests the actual data from the pim storage
+  service for a list of @see DataReferences. The request can
+  be parameterized by the part of the data your are interested in
+  and the acceptable mimetypes.
+
+  Example:
+
+  \code
+    SomeDataRequest request( references, &quot;ALL&quot;, QStringList( &quot;text/x-
+vcard&quot; ) );
+    if ( !request.exec() ) {
+      qDebug( &quot;Error: %s&quot;, qPrintable( request.errorString() ) );
+      return;
+    }
+
+    for ( int i = 0; i &lt; request.count(); ++i ) {
+      if ( request.mimetype( i ) == &quot;text/x-vcard&quot; ) {
+        doSomeConvertion( request.data( i ) );
+      }
+    }
+  \endcode
+ " isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="198" isRoot="false" isAbstract="false" name="DataRequest" >
+     <UML:GeneralizableElement.generalization>
+      <UML:Generalization xmi.idref="200" />
+     </UML:GeneralizableElement.generalization>
+    </UML:Class>
+    <UML:Class comment="  This class encapsulates a reference to a pim object in
+  the pim storage service.
+ " isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="199" isRoot="false" isAbstract="false" name="DataReference" />
+    <UML:Class comment=" This class queries the pim storage service for a list of
+  @see DataReferences, which match a given query pattern.
+
+  Example for synchronous API:
+
+  \code
+    Query query;
+    query.setQueryPattern( &quot;mimetype=message/rfc822; folder=/inbox; d
+ate>=2005-12-31;&quot; );
+    query.exec();
+    const QList&lt;PIM::DataReference> result = query.result();
+  \endcode
+ */
+" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="206" isRoot="false" isAbstract="false" name="Query" >
+     <UML:GeneralizableElement.generalization>
+      <UML:Generalization xmi.idref="207" />
+     </UML:GeneralizableElement.generalization>
+    </UML:Class>
+    <UML:Class comment="  This class can be used to retrieve the complete or partial collecti
+on tree
+  of the PIM storage service.
+
+  It returns a QHash of references to PIM::Collection objects.
+
+  @todo Add partial collection retrieval (eg. only collections contai
+ning contacts).
+*" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="210" isRoot="false" isAbstract="false" name="CollectionListJob" >
+     <UML:GeneralizableElement.generalization>
+      <UML:Generalization xmi.idref="211" />
+     </UML:GeneralizableElement.generalization>
+    </UML:Class>
+    <UML:Class comment="  Model to handle a collection tree.
+
+  @todo Add support for collection filtering, eg. deal only with collections
+  containing contacts.
+
+  @todo Split into generic and KDE dependent parts?
+" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="214" isRoot="false" isAbstract="false" name="CollectionModel" />
+    <UML:Class isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="215" isRoot="false" isAbstract="false" name="CollectionView" />
+    <UML:Class comment="  Helper functions to parse IMAP responses.
+  @todo Not really Akonadi specific, move somewhere else.
+" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="219" isRoot="false" isAbstract="false" name="ImapParser" />
+    <UML:Class comment=" A job queue. All jobs you put in here are executed sequentially. JobQueue can
+  be used as a parent for jobs to share the same connection to the Akonadi backend. Do not start enqueued jobs manually!
+" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="221" isRoot="false" isAbstract="false" name="JobQueue" />
+    <UML:Class comment="f  Extended modell for message collections.
+  Supports columns for message unread/total counts.
+" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="225" isRoot="false" isAbstract="false" name="MessageCollectionModel" >
+     <UML:GeneralizableElement.generalization>
+      <UML:Generalization xmi.idref="226" />
+     </UML:GeneralizableElement.generalization>
+    </UML:Class>
+    <UML:Class comment="  A collection of messages, eg. emails or news articles.
+" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="229" isRoot="false" isAbstract="false" name="MessageCollectionAttribute" >
+     <UML:GeneralizableElement.generalization>
+      <UML:Generalization xmi.idref="230" />
+     </UML:GeneralizableElement.generalization>
+    </UML:Class>
+    <UML:Class comment="Query for mails" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="239" isRoot="false" isAbstract="false" name="EmailQuery" >
+     <UML:GeneralizableElement.generalization>
+      <UML:Generalization xmi.idref="240" />
+     </UML:GeneralizableElement.generalization>
+    </UML:Class>
+    <UML:Class isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="243" isRoot="false" isAbstract="false" name="ContactQuery" >
+     <UML:GeneralizableElement.generalization>
+      <UML:Generalization xmi.idref="244" />
+     </UML:GeneralizableElement.generalization>
+    </UML:Class>
+    <UML:Class comment="Monitors an Item or Collection for changes and emits signals if som
+e
+  of these objects are changed or removed or new ones are added to th
+e storage
+  backend.
+
+  @todo: support un-monitoring
+  @todo: distinguish between monitoring collection properties and col
+lection content.
+" isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="247" isRoot="false" isAbstract="false" name="Monitor" />
+    <UML:Association isSpecification="false" visibility="public" namespace="m1" xmi.id="152" name="" >
+     <UML:Association.connection>
+      <UML:AssociationEnd isSpecification="false" visibility="public" changeability="changeable" isNavigable="false" xmi.id="153" aggregation="none" type="145" name="" />
+      <UML:AssociationEnd isSpecification="false" visibility="public" changeability="changeable" isNavigable="true" xmi.id="154" aggregation="none" type="150" name="" />
+     </UML:Association.connection>
+    </UML:Association>
+    <UML:Association isSpecification="false" visibility="public" namespace="m1" xmi.id="156" name="" >
+     <UML:Association.connection>
+      <UML:AssociationEnd isSpecification="false" visibility="public" changeability="changeable" isNavigable="false" xmi.id="157" aggregation="none" type="147" name="" />
+      <UML:AssociationEnd isSpecification="false" visibility="public" changeability="changeable" isNavigable="true" xmi.id="158" aggregation="none" type="155" name="" />
+     </UML:Association.connection>
+    </UML:Association>
+    <UML:Generalization isSpecification="false" child="147" visibility="public" namespace="m1" xmi.id="159" parent="146" discriminator="" name="" />
+    <UML:Generalization isSpecification="false" child="145" visibility="public" namespace="m1" xmi.id="163" parent="150" discriminator="" name="" />
+    <UML:Generalization isSpecification="false" child="147" visibility="public" namespace="m1" xmi.id="166" parent="155" discriminator="" name="" />
+    <UML:Association isSpecification="false" visibility="public" namespace="m1" xmi.id="171" name="" >
+     <UML:Association.connection>
+      <UML:AssociationEnd isSpecification="false" visibility="public" changeability="changeable" isNavigable="false" xmi.id="172" aggregation="none" type="169" name="" />
+      <UML:AssociationEnd isSpecification="false" visibility="public" changeability="changeable" isNavigable="true" xmi.id="173" aggregation="none" type="170" name="" />
+     </UML:Association.connection>
+    </UML:Association>
+    <UML:Association isSpecification="false" visibility="public" namespace="m1" xmi.id="174" name="" >
+     <UML:Association.connection>
+      <UML:AssociationEnd isSpecification="false" visibility="public" changeability="changeable" isNavigable="false" xmi.id="175" aggregation="none" type="169" name="" multiplicity="0-1" />
+      <UML:AssociationEnd isSpecification="false" visibility="public" changeability="changeable" isNavigable="true" xmi.id="176" aggregation="none" type="170" name="" />
+     </UML:Association.connection>
+    </UML:Association>
+    <UML:Generalization isSpecification="false" child="179" visibility="public" namespace="m1" xmi.id="180" parent="155" discriminator="" name="" />
+    <UML:Generalization isSpecification="false" child="183" visibility="public" namespace="m1" xmi.id="184" parent="155" discriminator="" name="" />
+    <UML:Generalization isSpecification="false" child="187" visibility="public" namespace="m1" xmi.id="188" parent="155" discriminator="" name="" />
+    <UML:Generalization isSpecification="false" child="191" visibility="public" namespace="m1" xmi.id="192" parent="155" discriminator="" name="" />
+    <UML:Generalization isSpecification="false" child="148" visibility="public" namespace="m1" xmi.id="195" parent="155" discriminator="" name="" />
+    <UML:Generalization isSpecification="false" child="198" visibility="public" namespace="m1" xmi.id="200" parent="155" discriminator="" name="" />
+    <UML:Association isSpecification="false" visibility="public" namespace="m1" xmi.id="203" name="" >
+     <UML:Association.connection>
+      <UML:AssociationEnd isSpecification="false" visibility="public" changeability="changeable" isNavigable="false" xmi.id="204" aggregation="none" type="198" name="" />
+      <UML:AssociationEnd isSpecification="false" visibility="public" changeability="changeable" isNavigable="true" xmi.id="205" aggregation="none" type="199" name="" />
+     </UML:Association.connection>
+    </UML:Association>
+    <UML:Generalization isSpecification="false" child="206" visibility="public" namespace="m1" xmi.id="207" parent="155" discriminator="" name="" />
+    <UML:Generalization isSpecification="false" child="210" visibility="public" namespace="m1" xmi.id="211" parent="155" discriminator="" name="" />
+    <UML:Association isSpecification="false" visibility="public" namespace="m1" xmi.id="216" name="" >
+     <UML:Association.connection>
+      <UML:AssociationEnd isSpecification="false" visibility="public" changeability="changeable" isNavigable="false" xmi.id="217" aggregation="none" type="215" name="" />
+      <UML:AssociationEnd isSpecification="false" visibility="public" changeability="changeable" isNavigable="true" xmi.id="218" aggregation="none" type="214" name="" />
+     </UML:Association.connection>
+    </UML:Association>
+    <UML:Association isSpecification="false" visibility="public" namespace="m1" xmi.id="222" name="" >
+     <UML:Association.connection>
+      <UML:AssociationEnd isSpecification="false" visibility="public" changeability="changeable" isNavigable="false" xmi.id="223" aggregation="none" type="221" name="" />
+      <UML:AssociationEnd isSpecification="false" visibility="public" changeability="changeable" isNavigable="true" xmi.id="224" aggregation="none" type="155" name="" />
+     </UML:Association.connection>
+    </UML:Association>
+    <UML:Generalization isSpecification="false" child="225" visibility="public" namespace="m1" xmi.id="226" parent="214" discriminator="" name="" />
+    <UML:Generalization isSpecification="false" child="229" visibility="public" namespace="m1" xmi.id="230" parent="170" discriminator="" name="" />
+    <UML:Association isSpecification="false" visibility="public" namespace="m1" xmi.id="233" name="" >
+     <UML:Association.connection>
+      <UML:AssociationEnd isSpecification="false" visibility="public" changeability="changeable" isNavigable="false" xmi.id="234" aggregation="none" type="225" name="" />
+      <UML:AssociationEnd isSpecification="false" visibility="public" changeability="changeable" isNavigable="true" xmi.id="235" aggregation="none" type="229" name="" />
+     </UML:Association.connection>
+    </UML:Association>
+    <UML:Association isSpecification="false" visibility="public" namespace="m1" xmi.id="236" name="" >
+     <UML:Association.connection>
+      <UML:AssociationEnd isSpecification="false" visibility="public" changeability="changeable" isNavigable="false" xmi.id="237" aggregation="none" type="146" name="" />
+      <UML:AssociationEnd isSpecification="false" visibility="public" changeability="changeable" isNavigable="true" xmi.id="238" aggregation="none" type="155" name="" />
+     </UML:Association.connection>
+    </UML:Association>
+    <UML:Generalization isSpecification="false" child="239" visibility="public" namespace="m1" xmi.id="240" parent="206" discriminator="" name="" />
+    <UML:Generalization isSpecification="false" child="243" visibility="public" namespace="m1" xmi.id="244" parent="206" discriminator="" name="" />
+    <UML:Association isSpecification="false" visibility="public" namespace="m1" xmi.id="248" name="" >
+     <UML:Association.connection>
+      <UML:AssociationEnd isSpecification="false" visibility="public" changeability="changeable" isNavigable="false" xmi.id="249" aggregation="none" type="247" name="" />
+      <UML:AssociationEnd isSpecification="false" visibility="public" changeability="changeable" isNavigable="true" xmi.id="250" aggregation="none" type="199" name="" />
+     </UML:Association.connection>
+    </UML:Association>
+   </UML:Namespace.ownedElement>
+  </UML:Model>
+ </XMI.content>
+ <XMI.extensions xmi.extender="umbrello" >
+  <docsettings viewid="129" documentation="R/I/Query for contacts" uniqueid="251" />
+  <diagrams>
+   <diagram snapgrid="0" showattsig="1" fillcolor="#ffffc0" linewidth="0" zoom="100" showgrid="0" showopsig="1" usefillcolor="1" snapx="10" canvaswidth="957" snapy="10" showatts="1" xmi.id="129" documentation="" type="402" showops="1" showpackage="0" name="class diagram" localid="900000" showstereotype="0" showscope="1" snapcsgrid="0" font="Sans Serif,10,-1,5,50,0,0,0,0,0" linecolor="#ff0000" canvasheight="619" >
+    <widgets>
+     <classwidget usesdiagramfillcolour="1" width="66" showattsigs="601" usesdiagramusefillcolour="1" x="502" y="264" showopsigs="601" linewidth="none" fillcolour="none" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="145" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="none" />
+     <classwidget usesdiagramfillcolour="0" width="102" showattsigs="601" usesdiagramusefillcolour="0" x="237" y="219" showopsigs="601" linewidth="none" fillcolour="#ffffc0" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="146" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="#ff0000" />
+     <classwidget usesdiagramfillcolour="1" width="121" showattsigs="601" usesdiagramusefillcolour="1" x="198" y="578" showopsigs="601" linewidth="none" fillcolour="none" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="147" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="none" />
+     <classwidget usesdiagramfillcolour="0" width="103" showattsigs="601" usesdiagramusefillcolour="0" x="198" y="326" showopsigs="601" linewidth="none" fillcolour="#ffffc0" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="148" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="#ff0000" />
+     <classwidget usesdiagramfillcolour="1" width="38" showattsigs="601" usesdiagramusefillcolour="1" x="599" y="264" showopsigs="601" linewidth="none" fillcolour="none" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="150" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="none" />
+     <classwidget usesdiagramfillcolour="1" width="34" showattsigs="601" usesdiagramusefillcolour="1" x="420" y="442" showopsigs="601" linewidth="none" fillcolour="none" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="155" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="none" />
+     <classwidget usesdiagramfillcolour="0" width="73" showattsigs="601" usesdiagramusefillcolour="0" x="705" y="206" showopsigs="601" linewidth="none" fillcolour="#ffffc0" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="169" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="#ff0000" />
+     <classwidget usesdiagramfillcolour="1" width="125" showattsigs="601" usesdiagramusefillcolour="1" x="677" y="125" showopsigs="601" linewidth="none" fillcolour="none" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="170" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="none" />
+     <classwidget usesdiagramfillcolour="1" width="133" showattsigs="601" usesdiagramusefillcolour="1" x="653" y="582" showopsigs="601" linewidth="none" fillcolour="none" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="179" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="none" />
+     <classwidget usesdiagramfillcolour="1" width="143" showattsigs="601" usesdiagramusefillcolour="1" x="487" y="578" showopsigs="601" linewidth="none" fillcolour="none" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="183" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="none" />
+     <classwidget usesdiagramfillcolour="0" width="131" showattsigs="601" usesdiagramusefillcolour="0" x="341" y="578" showopsigs="601" linewidth="none" fillcolour="#ffffc0" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="187" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="#ff0000" />
+     <classwidget usesdiagramfillcolour="1" width="107" showattsigs="601" usesdiagramusefillcolour="1" x="197" y="406" showopsigs="601" linewidth="none" fillcolour="none" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="191" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="none" />
+     <classwidget usesdiagramfillcolour="0" width="89" showattsigs="601" usesdiagramusefillcolour="0" x="697" y="478" showopsigs="601" linewidth="none" fillcolour="#ffffc0" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="198" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="#ff0000" />
+     <classwidget usesdiagramfillcolour="0" width="97" showattsigs="601" usesdiagramusefillcolour="0" x="689" y="344" showopsigs="601" linewidth="none" fillcolour="#ffffc0" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="199" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="#ff0000" />
+     <classwidget usesdiagramfillcolour="0" width="49" showattsigs="601" usesdiagramusefillcolour="0" x="737" y="525" showopsigs="601" linewidth="none" fillcolour="#ffffc0" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="206" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="#ff0000" />
+     <classwidget usesdiagramfillcolour="0" width="119" showattsigs="601" usesdiagramusefillcolour="0" x="198" y="459" showopsigs="601" linewidth="none" fillcolour="#ffffc0" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="210" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="#ff0000" />
+     <classwidget usesdiagramfillcolour="0" width="109" showattsigs="601" usesdiagramusefillcolour="0" x="201" y="37" showopsigs="601" linewidth="none" fillcolour="#ffffc0" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="214" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="#ff0000" />
+     <classwidget usesdiagramfillcolour="1" width="102" showattsigs="601" usesdiagramusefillcolour="1" x="56" y="36" showopsigs="601" linewidth="none" fillcolour="none" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="215" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="none" />
+     <classwidget usesdiagramfillcolour="0" width="83" showattsigs="601" usesdiagramusefillcolour="0" x="46" y="373" showopsigs="601" linewidth="none" fillcolour="#ffffc0" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="219" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="#ff0000" />
+     <notewidget usesdiagramfillcolour="1" width="119" usesdiagramusefillcolour="1" x="19" y="251" linewidth="none" fillcolour="none" height="90" usefillcolor="1" isinstance="0" xmi.id="220" showstereotype="1" text="Move outside libakonadi, or make private?" font="Sans Serif,10,-1,5,50,0,0,0,0,0" linecolor="none" />
+     <classwidget usesdiagramfillcolour="1" width="73" showattsigs="601" usesdiagramusefillcolour="1" x="379" y="222" showopsigs="601" linewidth="none" fillcolour="none" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="221" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="none" />
+     <classwidget usesdiagramfillcolour="0" width="163" showattsigs="601" usesdiagramusefillcolour="0" x="362" y="38" showopsigs="601" linewidth="none" fillcolour="#ffffc0" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="225" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="#ff0000" />
+     <classwidget usesdiagramfillcolour="0" width="179" showattsigs="601" usesdiagramusefillcolour="0" x="615" y="40" showopsigs="601" linewidth="none" fillcolour="#ffffc0" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="229" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="#ff0000" />
+     <classwidget usesdiagramfillcolour="0" width="85" showattsigs="601" usesdiagramusefillcolour="0" x="860" y="472" showopsigs="601" linewidth="none" fillcolour="#ffffc0" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="239" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="#ff0000" />
+     <classwidget usesdiagramfillcolour="1" width="96" showattsigs="601" usesdiagramusefillcolour="1" x="849" y="557" showopsigs="601" linewidth="none" fillcolour="none" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="243" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="none" />
+     <classwidget usesdiagramfillcolour="0" width="58" showattsigs="601" usesdiagramusefillcolour="0" x="728" y="263" showopsigs="601" linewidth="none" fillcolour="#ffffc0" height="32" usefillcolor="1" showpubliconly="0" showattributes="1" isinstance="0" xmi.id="247" showoperations="1" showpackage="0" showscope="1" font="Sans Serif,10,-1,5,75,0,0,0,0,0" linecolor="#ff0000" />
+     <notewidget usesdiagramfillcolour="1" width="100" usesdiagramusefillcolour="1" x="816" y="241" linewidth="none" fillcolour="none" height="80" usefillcolor="1" isinstance="0" xmi.id="251" text="Receives notifications of monitored items over DBus" font="Sans Serif,10,-1,5,50,0,0,0,0,0" linecolor="none" />
+    </widgets>
+    <messages/>
+    <associations>
+     <assocwidget totalcounta="2" indexa="1" totalcountb="2" indexb="1" linewidth="none" widgetbid="150" widgetaid="145" xmi.id="163" linecolor="none" >
+      <linepath>
+       <startpoint startx="568" starty="280" />
+       <endpoint endx="599" endy="280" />
+      </linepath>
+     </assocwidget>
+     <assocwidget totalcounta="2" indexa="1" totalcountb="5" indexb="4" linewidth="none" widgetbid="155" widgetaid="147" xmi.id="166" linecolor="none" >
+      <linepath>
+       <startpoint startx="258" starty="578" />
+       <endpoint endx="420" endy="467" />
+      </linepath>
+     </assocwidget>
+     <assocwidget totalcounta="2" indexa="1" totalcountb="2" indexb="1" linewidth="none" widgetbid="170" widgetaid="169" xmi.id="174" linecolor="none" >
+      <linepath>
+       <startpoint startx="741" starty="206" />
+       <endpoint endx="739" endy="157" />
+      </linepath>
+      <floatingtext usesdiagramfillcolour="1" width="32" usesdiagramusefillcolour="1" x="708" y="177" linewidth="none" posttext="" role="701" fillcolour="none" height="32" usefillcolor="1" pretext="" isinstance="0" xmi.id="178" showstereotype="1" text="0-1" font="Sans Serif,10,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </assocwidget>
+     <assocwidget totalcounta="2" indexa="1" totalcountb="4" indexb="3" linewidth="none" widgetbid="155" widgetaid="179" xmi.id="180" linecolor="none" >
+      <linepath>
+       <startpoint startx="719" starty="582" />
+       <endpoint endx="454" endy="466" />
+      </linepath>
+     </assocwidget>
+     <assocwidget totalcounta="2" indexa="1" totalcountb="3" indexb="2" linewidth="none" widgetbid="155" widgetaid="183" xmi.id="184" linecolor="none" >
+      <linepath>
+       <startpoint startx="558" starty="578" />
+       <endpoint endx="442" endy="474" />
+      </linepath>
+     </assocwidget>
+     <assocwidget totalcounta="2" indexa="1" totalcountb="3" indexb="1" linewidth="none" widgetbid="155" widgetaid="187" xmi.id="188" linecolor="none" >
+      <linepath>
+       <startpoint startx="406" starty="578" />
+       <endpoint endx="431" endy="474" />
+      </linepath>
+     </assocwidget>
+     <assocwidget totalcounta="2" indexa="1" totalcountb="5" indexb="2" linewidth="none" widgetbid="155" widgetaid="191" xmi.id="192" linecolor="none" >
+      <linepath>
+       <startpoint startx="304" starty="422" />
+       <endpoint endx="420" endy="454" />
+      </linepath>
+     </assocwidget>
+     <assocwidget totalcounta="2" indexa="1" totalcountb="5" indexb="1" linewidth="none" widgetbid="155" widgetaid="148" xmi.id="195" linecolor="none" >
+      <linepath>
+       <startpoint startx="249" starty="358" />
+       <endpoint endx="420" endy="448" />
+      </linepath>
+     </assocwidget>
+     <assocwidget totalcounta="2" indexa="1" totalcountb="4" indexb="1" linewidth="none" widgetbid="155" widgetaid="198" xmi.id="200" linecolor="none" >
+      <linepath>
+       <startpoint startx="697" starty="494" />
+       <endpoint endx="454" endy="450" />
+      </linepath>
+     </assocwidget>
+     <assocwidget totalcounta="2" indexa="1" totalcountb="2" indexb="1" linewidth="none" widgetbid="199" widgetaid="198" xmi.id="203" linecolor="none" >
+      <linepath>
+       <startpoint startx="741" starty="478" />
+       <endpoint endx="737" endy="376" />
+      </linepath>
+     </assocwidget>
+     <assocwidget totalcounta="2" indexa="1" totalcountb="4" indexb="2" linewidth="none" widgetbid="155" widgetaid="206" xmi.id="207" linecolor="none" >
+      <linepath>
+       <startpoint startx="737" starty="541" />
+       <endpoint endx="454" endy="458" />
+      </linepath>
+     </assocwidget>
+     <assocwidget totalcounta="2" indexa="1" totalcountb="5" indexb="3" linewidth="none" widgetbid="155" widgetaid="210" xmi.id="211" linecolor="none" >
+      <linepath>
+       <startpoint startx="317" starty="475" />
+       <endpoint endx="420" endy="461" />
+      </linepath>
+     </assocwidget>
+     <assocwidget totalcounta="2" indexa="1" totalcountb="2" indexb="1" linewidth="none" widgetbid="214" widgetaid="215" xmi.id="216" linecolor="none" >
+      <linepath>
+       <startpoint startx="158" starty="52" />
+       <endpoint endx="201" endy="53" />
+      </linepath>
+     </assocwidget>
+     <assocwidget totalcounta="2" indexa="1" totalcountb="3" indexb="2" linewidth="none" widgetbid="155" widgetaid="221" xmi.id="222" linecolor="none" >
+      <linepath>
+       <startpoint startx="415" starty="254" />
+       <endpoint endx="442" endy="442" />
+      </linepath>
+     </assocwidget>
+     <assocwidget totalcounta="2" indexa="1" totalcountb="2" indexb="1" linewidth="none" widgetbid="214" widgetaid="225" xmi.id="226" linecolor="none" >
+      <linepath>
+       <startpoint startx="362" starty="54" />
+       <endpoint endx="310" endy="53" />
+      </linepath>
+     </assocwidget>
+     <assocwidget totalcounta="2" indexa="1" totalcountb="2" indexb="1" linewidth="none" widgetbid="170" widgetaid="229" xmi.id="230" linecolor="none" >
+      <linepath>
+       <startpoint startx="704" starty="72" />
+       <endpoint endx="739" endy="125" />
+      </linepath>
+     </assocwidget>
+     <assocwidget totalcounta="2" indexa="1" totalcountb="2" indexb="1" linewidth="none" widgetbid="229" widgetaid="225" xmi.id="233" linecolor="none" >
+      <linepath>
+       <startpoint startx="525" starty="54" />
+       <endpoint endx="615" endy="56" />
+      </linepath>
+     </assocwidget>
+     <assocwidget totalcounta="2" indexa="1" totalcountb="3" indexb="1" linewidth="none" widgetbid="155" widgetaid="146" xmi.id="236" linecolor="none" >
+      <linepath>
+       <startpoint startx="288" starty="251" />
+       <endpoint endx="431" endy="442" />
+      </linepath>
+     </assocwidget>
+     <assocwidget totalcounta="2" indexa="1" totalcountb="3" indexb="1" linewidth="none" widgetbid="206" widgetaid="239" xmi.id="240" linecolor="none" >
+      <linepath>
+       <startpoint startx="860" starty="488" />
+       <endpoint endx="786" endy="535" />
+      </linepath>
+     </assocwidget>
+     <assocwidget totalcounta="2" indexa="1" totalcountb="3" indexb="2" linewidth="none" widgetbid="206" widgetaid="243" xmi.id="244" linecolor="none" >
+      <linepath>
+       <startpoint startx="849" starty="573" />
+       <endpoint endx="786" endy="546" />
+      </linepath>
+     </assocwidget>
+     <assocwidget totalcounta="2" indexa="1" totalcountb="2" indexb="1" linewidth="none" widgetbid="199" widgetaid="247" xmi.id="248" linecolor="none" >
+      <linepath>
+       <startpoint startx="757" starty="295" />
+       <endpoint endx="737" endy="344" />
+      </linepath>
+     </assocwidget>
+    </associations>
+   </diagram>
+  </diagrams>
+  <listview>
+   <listitem open="1" type="800" label="Views" >
+    <listitem open="1" type="801" label="Logical View" >
+     <listitem open="1" type="813" id="169" />
+     <listitem open="1" type="813" id="170" />
+     <listitem open="1" type="813" id="179" />
+     <listitem open="1" type="813" id="210" />
+     <listitem open="1" type="813" id="214" />
+     <listitem open="1" type="813" id="183" />
+     <listitem open="1" type="813" id="187" />
+     <listitem open="1" type="813" id="215" />
+     <listitem open="1" type="813" id="243" />
+     <listitem open="1" type="813" id="199" />
+     <listitem open="1" type="813" id="198" />
+     <listitem open="1" type="813" id="239" />
+     <listitem open="1" type="813" id="219" />
+     <listitem open="1" type="813" id="150" />
+     <listitem open="1" type="813" id="191" />
+     <listitem open="1" type="813" id="155" />
+     <listitem open="1" type="813" id="221" />
+     <listitem open="1" type="813" id="145" />
+     <listitem open="0" type="813" id="229" />
+     <listitem open="1" type="813" id="225" />
+     <listitem open="1" type="813" id="147" />
+     <listitem open="1" type="813" id="146" />
+     <listitem open="1" type="813" id="148" />
+     <listitem open="1" type="813" id="247" />
+     <listitem open="1" type="813" id="206" />
+     <listitem open="0" type="830" label="Datatypes" >
+      <listitem open="1" type="829" id="133" />
+      <listitem open="1" type="829" id="132" />
+      <listitem open="1" type="829" id="135" />
+      <listitem open="1" type="829" id="134" />
+      <listitem open="1" type="829" id="130" />
+      <listitem open="1" type="829" id="137" />
+      <listitem open="1" type="829" id="136" />
+      <listitem open="1" type="829" id="141" />
+      <listitem open="1" type="829" id="138" />
+      <listitem open="1" type="829" id="140" />
+      <listitem open="1" type="829" id="139" />
+     </listitem>
+    </listitem>
+    <listitem open="1" type="802" label="Use Case View" />
+    <listitem open="1" type="821" label="Component View" />
+    <listitem open="1" type="827" label="Deployment View" />
+    <listitem open="1" type="836" label="Entity Relationship Model" />
+   </listitem>
+  </listview>
+  <codegeneration>
+   <codegenerator language="C++" >
+    <classifiercodedocument writeOutCode="true" package="-1" id="145" parent_class="145" fileExt=".cpp" fileName="Message" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;message.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        Message.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:40:56&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="163" field_type="140121136" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="163" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="163" tag="" canDelete="false" writeOutText="false" classfield_id="163" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="163" tag="" canDelete="false" writeOutText="false" classfield_id="163" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="163" tag="" canDelete="false" writeOutText="false" classfield_id="163" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="163" tag="" canDelete="false" writeOutText="false" classfield_id="163" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="163" tag="" canDelete="false" writeOutText="false" classfield_id="163" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="146" parent_class="146" fileExt=".cpp" fileName="MessageModel" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;messagemodel.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks>
+        <codeaccessormethod accessType="0" parent_id="236" tag="hblock_tag_0" canDelete="false" writeOutText="false" classfield_id="236" role_id="0" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+        <codeaccessormethod accessType="1" parent_id="236" tag="hblock_tag_1" canDelete="false" writeOutText="false" classfield_id="236" role_id="0" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+        <codeaccessormethod accessType="2" parent_id="236" tag="hblock_tag_2" canDelete="false" writeOutText="false" classfield_id="236" role_id="0" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+        <codeaccessormethod accessType="3" parent_id="236" tag="hblock_tag_3" canDelete="false" writeOutText="false" classfield_id="236" role_id="0" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+        <codeaccessormethod accessType="4" parent_id="236" tag="hblock_tag_4" canDelete="false" writeOutText="false" classfield_id="236" role_id="0" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+       </textblocks>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        MessageModel.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:26:33&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="236" field_type="1" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="236" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="236" tag="hblock_tag_0" canDelete="false" writeOutText="false" classfield_id="236" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="236" tag="hblock_tag_1" canDelete="false" writeOutText="false" classfield_id="236" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="236" tag="hblock_tag_2" canDelete="false" writeOutText="false" classfield_id="236" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="236" tag="hblock_tag_3" canDelete="false" writeOutText="false" classfield_id="236" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="236" tag="hblock_tag_4" canDelete="false" writeOutText="false" classfield_id="236" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="147" parent_class="147" fileExt=".cpp" fileName="MessageFetchJob" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;messagefetchjob.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        MessageFetchJob.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:11:38&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="166" field_type="145670016" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="166" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="166" tag="" canDelete="false" writeOutText="false" classfield_id="166" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="166" tag="" canDelete="false" writeOutText="false" classfield_id="166" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="166" tag="" canDelete="false" writeOutText="false" classfield_id="166" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="166" tag="" canDelete="false" writeOutText="false" classfield_id="166" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="166" tag="" canDelete="false" writeOutText="false" classfield_id="166" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="148" parent_class="148" fileExt=".cpp" fileName="MessageQuery" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;messagequery.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        MessageQuery.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:28:16&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="195" field_type="2" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="195" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="195" tag="" canDelete="false" writeOutText="false" classfield_id="195" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="195" tag="" canDelete="false" writeOutText="false" classfield_id="195" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="195" tag="" canDelete="false" writeOutText="false" classfield_id="195" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="195" tag="" canDelete="false" writeOutText="false" classfield_id="195" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="195" tag="" canDelete="false" writeOutText="false" classfield_id="195" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="150" parent_class="150" fileExt=".cpp" fileName="Item" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;item.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks>
+        <codeaccessormethod accessType="0" parent_id="163" tag="hblock_tag_0" canDelete="false" writeOutText="false" classfield_id="163" role_id="1" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+        <codeaccessormethod accessType="1" parent_id="163" tag="hblock_tag_1" canDelete="false" writeOutText="false" classfield_id="163" role_id="1" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+        <codeaccessormethod accessType="2" parent_id="163" tag="hblock_tag_2" canDelete="false" writeOutText="false" classfield_id="163" role_id="1" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+        <codeaccessormethod accessType="3" parent_id="163" tag="hblock_tag_3" canDelete="false" writeOutText="false" classfield_id="163" role_id="1" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+        <codeaccessormethod accessType="4" parent_id="163" tag="hblock_tag_4" canDelete="false" writeOutText="false" classfield_id="163" role_id="1" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+       </textblocks>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        Item.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:40:53&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="163" field_type="0" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="163" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="163" tag="hblock_tag_0" canDelete="false" writeOutText="false" classfield_id="163" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="163" tag="hblock_tag_1" canDelete="false" writeOutText="false" classfield_id="163" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="163" tag="hblock_tag_2" canDelete="false" writeOutText="false" classfield_id="163" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="163" tag="hblock_tag_3" canDelete="false" writeOutText="false" classfield_id="163" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="163" tag="hblock_tag_4" canDelete="false" writeOutText="false" classfield_id="163" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="155" parent_class="155" fileExt=".cpp" fileName="Job" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;job.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks>
+        <codeaccessormethod accessType="0" parent_id="184" tag="hblock_tag_0" canDelete="false" writeOutText="false" classfield_id="184" role_id="1" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+        <codeaccessormethod accessType="1" parent_id="184" tag="hblock_tag_1" canDelete="false" writeOutText="false" classfield_id="184" role_id="1" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+        <codeaccessormethod accessType="2" parent_id="184" tag="hblock_tag_2" canDelete="false" writeOutText="false" classfield_id="184" role_id="1" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+        <codeaccessormethod accessType="3" parent_id="184" tag="hblock_tag_3" canDelete="false" writeOutText="false" classfield_id="184" role_id="1" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+        <codeaccessormethod accessType="4" parent_id="184" tag="hblock_tag_4" canDelete="false" writeOutText="false" classfield_id="184" role_id="1" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+       </textblocks>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        Job.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:26:02&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="166" field_type="143152328" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="166" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="166" tag="" canDelete="false" writeOutText="false" classfield_id="166" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="166" tag="" canDelete="false" writeOutText="false" classfield_id="166" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="166" tag="" canDelete="false" writeOutText="false" classfield_id="166" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="166" tag="" canDelete="false" writeOutText="false" classfield_id="166" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="166" tag="" canDelete="false" writeOutText="false" classfield_id="166" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="180" field_type="2097268" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="180" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="180" tag="" canDelete="false" writeOutText="false" classfield_id="180" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="180" tag="" canDelete="false" writeOutText="false" classfield_id="180" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="180" tag="" canDelete="false" writeOutText="false" classfield_id="180" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="180" tag="" canDelete="false" writeOutText="false" classfield_id="180" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="180" tag="" canDelete="false" writeOutText="false" classfield_id="180" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="184" field_type="0" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="184" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="184" tag="hblock_tag_0" canDelete="false" writeOutText="false" classfield_id="184" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="184" tag="hblock_tag_1" canDelete="false" writeOutText="false" classfield_id="184" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="184" tag="hblock_tag_2" canDelete="false" writeOutText="false" classfield_id="184" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="184" tag="hblock_tag_3" canDelete="false" writeOutText="false" classfield_id="184" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="184" tag="hblock_tag_4" canDelete="false" writeOutText="false" classfield_id="184" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="188" field_type="7077983" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="188" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="188" tag="" canDelete="false" writeOutText="false" classfield_id="188" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="188" tag="" canDelete="false" writeOutText="false" classfield_id="188" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="188" tag="" canDelete="false" writeOutText="false" classfield_id="188" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="188" tag="" canDelete="false" writeOutText="false" classfield_id="188" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="188" tag="" canDelete="false" writeOutText="false" classfield_id="188" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="192" field_type="7077983" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="192" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="192" tag="" canDelete="false" writeOutText="false" classfield_id="192" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="192" tag="" canDelete="false" writeOutText="false" classfield_id="192" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="192" tag="" canDelete="false" writeOutText="false" classfield_id="192" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="192" tag="" canDelete="false" writeOutText="false" classfield_id="192" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="192" tag="" canDelete="false" writeOutText="false" classfield_id="192" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="195" field_type="2097192" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="195" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="195" tag="" canDelete="false" writeOutText="false" classfield_id="195" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="195" tag="" canDelete="false" writeOutText="false" classfield_id="195" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="195" tag="" canDelete="false" writeOutText="false" classfield_id="195" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="195" tag="" canDelete="false" writeOutText="false" classfield_id="195" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="195" tag="" canDelete="false" writeOutText="false" classfield_id="195" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="200" field_type="655370" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="200" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="200" tag="" canDelete="false" writeOutText="false" classfield_id="200" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="200" tag="" canDelete="false" writeOutText="false" classfield_id="200" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="200" tag="" canDelete="false" writeOutText="false" classfield_id="200" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="200" tag="" canDelete="false" writeOutText="false" classfield_id="200" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="200" tag="" canDelete="false" writeOutText="false" classfield_id="200" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="207" field_type="6815860" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="207" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="207" tag="" canDelete="false" writeOutText="false" classfield_id="207" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="207" tag="" canDelete="false" writeOutText="false" classfield_id="207" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="207" tag="" canDelete="false" writeOutText="false" classfield_id="207" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="207" tag="" canDelete="false" writeOutText="false" classfield_id="207" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="207" tag="" canDelete="false" writeOutText="false" classfield_id="207" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="211" field_type="6815860" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="211" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="211" tag="" canDelete="false" writeOutText="false" classfield_id="211" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="211" tag="" canDelete="false" writeOutText="false" classfield_id="211" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="211" tag="" canDelete="false" writeOutText="false" classfield_id="211" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="211" tag="" canDelete="false" writeOutText="false" classfield_id="211" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="211" tag="" canDelete="false" writeOutText="false" classfield_id="211" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="222" field_type="7536745" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="222" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="222" tag="" canDelete="false" writeOutText="false" classfield_id="222" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="222" tag="" canDelete="false" writeOutText="false" classfield_id="222" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="222" tag="" canDelete="false" writeOutText="false" classfield_id="222" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="222" tag="" canDelete="false" writeOutText="false" classfield_id="222" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="222" tag="" canDelete="false" writeOutText="false" classfield_id="222" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="236" field_type="7733280" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="236" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="236" tag="" canDelete="false" writeOutText="false" classfield_id="236" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="236" tag="" canDelete="false" writeOutText="false" classfield_id="236" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="236" tag="" canDelete="false" writeOutText="false" classfield_id="236" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="236" tag="" canDelete="false" writeOutText="false" classfield_id="236" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="236" tag="" canDelete="false" writeOutText="false" classfield_id="236" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="169" parent_class="169" fileExt=".cpp" fileName="Collection" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;collection.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        Collection.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:43:16&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="171" field_type="140162656" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="171" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="171" tag="" canDelete="false" writeOutText="false" classfield_id="171" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="171" tag="" canDelete="false" writeOutText="false" classfield_id="171" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="171" tag="" canDelete="false" writeOutText="false" classfield_id="171" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="171" tag="" canDelete="false" writeOutText="false" classfield_id="171" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="171" tag="" canDelete="false" writeOutText="false" classfield_id="171" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="174" field_type="7077983" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="174" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="174" tag="" canDelete="false" writeOutText="false" classfield_id="174" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="174" tag="" canDelete="false" writeOutText="false" classfield_id="174" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="174" tag="" canDelete="false" writeOutText="false" classfield_id="174" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="174" tag="" canDelete="false" writeOutText="false" classfield_id="174" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="174" tag="" canDelete="false" writeOutText="false" classfield_id="174" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="170" parent_class="170" fileExt=".cpp" fileName="CollectionAttribute" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;collectionattribute.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        CollectionAttribute.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:43:08&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="171" field_type="6553635" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="171" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="171" tag="" canDelete="false" writeOutText="false" classfield_id="171" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="171" tag="" canDelete="false" writeOutText="false" classfield_id="171" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="171" tag="" canDelete="false" writeOutText="false" classfield_id="171" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="171" tag="" canDelete="false" writeOutText="false" classfield_id="171" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="171" tag="" canDelete="false" writeOutText="false" classfield_id="171" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="174" field_type="655370" initialValue=" new vector( )" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="174" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="174" tag="" canDelete="false" writeOutText="false" classfield_id="174" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="174" tag="" canDelete="false" writeOutText="false" classfield_id="174" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="174" tag="" canDelete="false" writeOutText="false" classfield_id="174" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="174" tag="" canDelete="false" writeOutText="false" classfield_id="174" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="174" tag="" canDelete="false" writeOutText="false" classfield_id="174" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="230" field_type="-2" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="230" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="230" tag="" canDelete="false" writeOutText="false" classfield_id="230" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="230" tag="" canDelete="false" writeOutText="false" classfield_id="230" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="230" tag="" canDelete="false" writeOutText="false" classfield_id="230" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="230" tag="" canDelete="false" writeOutText="false" classfield_id="230" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="230" tag="" canDelete="false" writeOutText="false" classfield_id="230" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="179" parent_class="179" fileExt=".cpp" fileName="CollectionCreateJob" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;collectioncreatejob.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        CollectionCreateJob.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:12:25&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="180" field_type="7077986" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="180" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="180" tag="" canDelete="false" writeOutText="false" classfield_id="180" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="180" tag="" canDelete="false" writeOutText="false" classfield_id="180" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="180" tag="" canDelete="false" writeOutText="false" classfield_id="180" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="180" tag="" canDelete="false" writeOutText="false" classfield_id="180" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="180" tag="" canDelete="false" writeOutText="false" classfield_id="180" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="183" parent_class="183" fileExt=".cpp" fileName="CollectionRenameJob" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;collectionrenamejob.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        CollectionRenameJob.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:11:27&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="184" field_type="7077986" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="184" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="184" tag="" canDelete="false" writeOutText="false" classfield_id="184" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="184" tag="" canDelete="false" writeOutText="false" classfield_id="184" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="184" tag="" canDelete="false" writeOutText="false" classfield_id="184" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="184" tag="" canDelete="false" writeOutText="false" classfield_id="184" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="184" tag="" canDelete="false" writeOutText="false" classfield_id="184" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="187" parent_class="187" fileExt=".cpp" fileName="CollectionSelectJob" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;collectionselectjob.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        CollectionSelectJob.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:11:22&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="188" field_type="145674040" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="188" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="188" tag="" canDelete="false" writeOutText="false" classfield_id="188" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="188" tag="" canDelete="false" writeOutText="false" classfield_id="188" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="188" tag="" canDelete="false" writeOutText="false" classfield_id="188" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="188" tag="" canDelete="false" writeOutText="false" classfield_id="188" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="188" tag="" canDelete="false" writeOutText="false" classfield_id="188" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="191" parent_class="191" fileExt=".cpp" fileName="ItemAppendJob" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;itemappendjob.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        ItemAppendJob.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:12:35&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="192" field_type="24" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="192" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="192" tag="" canDelete="false" writeOutText="false" classfield_id="192" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="192" tag="" canDelete="false" writeOutText="false" classfield_id="192" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="192" tag="" canDelete="false" writeOutText="false" classfield_id="192" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="192" tag="" canDelete="false" writeOutText="false" classfield_id="192" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="192" tag="" canDelete="false" writeOutText="false" classfield_id="192" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="198" parent_class="198" fileExt=".cpp" fileName="DataRequest" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;datarequest.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        DataRequest.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:12:24&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="200" field_type="97" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="200" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="200" tag="" canDelete="false" writeOutText="false" classfield_id="200" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="200" tag="" canDelete="false" writeOutText="false" classfield_id="200" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="200" tag="" canDelete="false" writeOutText="false" classfield_id="200" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="200" tag="" canDelete="false" writeOutText="false" classfield_id="200" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="200" tag="" canDelete="false" writeOutText="false" classfield_id="200" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="203" field_type="24" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="203" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="203" tag="" canDelete="false" writeOutText="false" classfield_id="203" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="203" tag="" canDelete="false" writeOutText="false" classfield_id="203" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="203" tag="" canDelete="false" writeOutText="false" classfield_id="203" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="203" tag="" canDelete="false" writeOutText="false" classfield_id="203" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="203" tag="" canDelete="false" writeOutText="false" classfield_id="203" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="199" parent_class="199" fileExt=".cpp" fileName="DataReference" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;datareference.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        DataReference.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:42:04&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="203" field_type="6881382" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="203" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="203" tag="" canDelete="false" writeOutText="false" classfield_id="203" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="203" tag="" canDelete="false" writeOutText="false" classfield_id="203" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="203" tag="" canDelete="false" writeOutText="false" classfield_id="203" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="203" tag="" canDelete="false" writeOutText="false" classfield_id="203" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="203" tag="" canDelete="false" writeOutText="false" classfield_id="203" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="248" field_type="7667828" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="248" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="248" tag="" canDelete="false" writeOutText="false" classfield_id="248" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="248" tag="" canDelete="false" writeOutText="false" classfield_id="248" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="248" tag="" canDelete="false" writeOutText="false" classfield_id="248" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="248" tag="" canDelete="false" writeOutText="false" classfield_id="248" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="248" tag="" canDelete="false" writeOutText="false" classfield_id="248" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="206" parent_class="206" fileExt=".cpp" fileName="Query" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;query.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks>
+        <codeaccessormethod accessType="0" parent_id="244" tag="hblock_tag_0" canDelete="false" writeOutText="false" classfield_id="244" role_id="1" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+        <codeaccessormethod accessType="1" parent_id="244" tag="hblock_tag_1" canDelete="false" writeOutText="false" classfield_id="244" role_id="1" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+        <codeaccessormethod accessType="2" parent_id="244" tag="hblock_tag_2" canDelete="false" writeOutText="false" classfield_id="244" role_id="1" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+        <codeaccessormethod accessType="3" parent_id="244" tag="hblock_tag_3" canDelete="false" writeOutText="false" classfield_id="244" role_id="1" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+        <codeaccessormethod accessType="4" parent_id="244" tag="hblock_tag_4" canDelete="false" writeOutText="false" classfield_id="244" role_id="1" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+       </textblocks>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        Query.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:37:00&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="207" field_type="20" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="207" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="207" tag="" canDelete="false" writeOutText="false" classfield_id="207" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="207" tag="" canDelete="false" writeOutText="false" classfield_id="207" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="207" tag="" canDelete="false" writeOutText="false" classfield_id="207" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="207" tag="" canDelete="false" writeOutText="false" classfield_id="207" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="207" tag="" canDelete="false" writeOutText="false" classfield_id="207" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="240" field_type="21" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="240" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="240" tag="" canDelete="false" writeOutText="false" classfield_id="240" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="240" tag="" canDelete="false" writeOutText="false" classfield_id="240" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="240" tag="" canDelete="false" writeOutText="false" classfield_id="240" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="240" tag="" canDelete="false" writeOutText="false" classfield_id="240" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="240" tag="" canDelete="false" writeOutText="false" classfield_id="240" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="244" field_type="0" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="244" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="244" tag="hblock_tag_0" canDelete="false" writeOutText="false" classfield_id="244" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="244" tag="hblock_tag_1" canDelete="false" writeOutText="false" classfield_id="244" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="244" tag="hblock_tag_2" canDelete="false" writeOutText="false" classfield_id="244" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="244" tag="hblock_tag_3" canDelete="false" writeOutText="false" classfield_id="244" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="244" tag="hblock_tag_4" canDelete="false" writeOutText="false" classfield_id="244" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="210" parent_class="210" fileExt=".cpp" fileName="CollectionListJob" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;collectionlistjob.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        CollectionListJob.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 16:54:59&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="211" field_type="6488169" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="211" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="211" tag="" canDelete="false" writeOutText="false" classfield_id="211" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="211" tag="" canDelete="false" writeOutText="false" classfield_id="211" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="211" tag="" canDelete="false" writeOutText="false" classfield_id="211" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="211" tag="" canDelete="false" writeOutText="false" classfield_id="211" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="211" tag="" canDelete="false" writeOutText="false" classfield_id="211" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="214" parent_class="214" fileExt=".cpp" fileName="CollectionModel" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;collectionmodel.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        CollectionModel.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:42:57&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="216" field_type="7209065" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="216" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="216" tag="" canDelete="false" writeOutText="false" classfield_id="216" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="216" tag="" canDelete="false" writeOutText="false" classfield_id="216" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="216" tag="" canDelete="false" writeOutText="false" classfield_id="216" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="216" tag="" canDelete="false" writeOutText="false" classfield_id="216" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="216" tag="" canDelete="false" writeOutText="false" classfield_id="216" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="226" field_type="7208970" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="226" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="226" tag="" canDelete="false" writeOutText="false" classfield_id="226" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="226" tag="" canDelete="false" writeOutText="false" classfield_id="226" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="226" tag="" canDelete="false" writeOutText="false" classfield_id="226" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="226" tag="" canDelete="false" writeOutText="false" classfield_id="226" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="226" tag="" canDelete="false" writeOutText="false" classfield_id="226" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="215" parent_class="215" fileExt=".cpp" fileName="CollectionView" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;collectionview.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        CollectionView.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:42:48&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="216" field_type="6881382" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="216" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="216" tag="" canDelete="false" writeOutText="false" classfield_id="216" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="216" tag="" canDelete="false" writeOutText="false" classfield_id="216" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="216" tag="" canDelete="false" writeOutText="false" classfield_id="216" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="216" tag="" canDelete="false" writeOutText="false" classfield_id="216" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="216" tag="" canDelete="false" writeOutText="false" classfield_id="216" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="219" parent_class="219" fileExt=".cpp" fileName="ImapParser" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;imapparser.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        ImapParser.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:04:38&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields/>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="221" parent_class="221" fileExt=".cpp" fileName="JobQueue" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;jobqueue.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        JobQueue.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:27:18&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="222" field_type="-1226399312" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="222" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="222" tag="" canDelete="false" writeOutText="false" classfield_id="222" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="222" tag="" canDelete="false" writeOutText="false" classfield_id="222" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="222" tag="" canDelete="false" writeOutText="false" classfield_id="222" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="222" tag="" canDelete="false" writeOutText="false" classfield_id="222" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="222" tag="" canDelete="false" writeOutText="false" classfield_id="222" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="225" parent_class="225" fileExt=".cpp" fileName="MessageCollectionModel" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;messagecollectionmodel.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks>
+        <codeaccessormethod accessType="0" parent_id="226" tag="hblock_tag_0" canDelete="false" writeOutText="false" classfield_id="226" role_id="0" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+        <codeaccessormethod accessType="1" parent_id="226" tag="hblock_tag_1" canDelete="false" writeOutText="false" classfield_id="226" role_id="0" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+        <codeaccessormethod accessType="2" parent_id="226" tag="hblock_tag_2" canDelete="false" writeOutText="false" classfield_id="226" role_id="0" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+        <codeaccessormethod accessType="3" parent_id="226" tag="hblock_tag_3" canDelete="false" writeOutText="false" classfield_id="226" role_id="0" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+        <codeaccessormethod accessType="4" parent_id="226" tag="hblock_tag_4" canDelete="false" writeOutText="false" classfield_id="226" role_id="0" >
+         <header>
+          <cppcodedocumentation tag="" />
+         </header>
+        </codeaccessormethod>
+       </textblocks>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        MessageCollectionModel.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:42:54&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="226" field_type="0" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="226" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="226" tag="hblock_tag_0" canDelete="false" writeOutText="false" classfield_id="226" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="226" tag="hblock_tag_1" canDelete="false" writeOutText="false" classfield_id="226" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="226" tag="hblock_tag_2" canDelete="false" writeOutText="false" classfield_id="226" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="226" tag="hblock_tag_3" canDelete="false" writeOutText="false" classfield_id="226" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="226" tag="hblock_tag_4" canDelete="false" writeOutText="false" classfield_id="226" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="233" field_type="7340064" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="233" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="233" tag="" canDelete="false" writeOutText="false" classfield_id="233" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="233" tag="" canDelete="false" writeOutText="false" classfield_id="233" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="233" tag="" canDelete="false" writeOutText="false" classfield_id="233" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="233" tag="" canDelete="false" writeOutText="false" classfield_id="233" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="233" tag="" canDelete="false" writeOutText="false" classfield_id="233" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="229" parent_class="229" fileExt=".cpp" fileName="MessageCollectionAttribute" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;messagecollectionattribute.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        MessageCollectionAttribute.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:43:06&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="230" field_type="6619252" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="230" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="230" tag="" canDelete="false" writeOutText="false" classfield_id="230" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="230" tag="" canDelete="false" writeOutText="false" classfield_id="230" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="230" tag="" canDelete="false" writeOutText="false" classfield_id="230" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="230" tag="" canDelete="false" writeOutText="false" classfield_id="230" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="230" tag="" canDelete="false" writeOutText="false" classfield_id="230" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="233" field_type="6619252" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="233" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="233" tag="" canDelete="false" writeOutText="false" classfield_id="233" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="233" tag="" canDelete="false" writeOutText="false" classfield_id="233" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="233" tag="" canDelete="false" writeOutText="false" classfield_id="233" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="233" tag="" canDelete="false" writeOutText="false" classfield_id="233" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="233" tag="" canDelete="false" writeOutText="false" classfield_id="233" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="239" parent_class="239" fileExt=".cpp" fileName="EmailQuery" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;emailquery.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        EmailQuery.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:39:18&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="240" field_type="2752554" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="240" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="240" tag="" canDelete="false" writeOutText="false" classfield_id="240" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="240" tag="" canDelete="false" writeOutText="false" classfield_id="240" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="240" tag="" canDelete="false" writeOutText="false" classfield_id="240" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="240" tag="" canDelete="false" writeOutText="false" classfield_id="240" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="240" tag="" canDelete="false" writeOutText="false" classfield_id="240" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="243" parent_class="243" fileExt=".cpp" fileName="ContactQuery" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;contactquery.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        ContactQuery.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:36:39&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="244" field_type="7471205" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="244" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="244" tag="" canDelete="false" writeOutText="false" classfield_id="244" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="244" tag="" canDelete="false" writeOutText="false" classfield_id="244" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="244" tag="" canDelete="false" writeOutText="false" classfield_id="244" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="244" tag="" canDelete="false" writeOutText="false" classfield_id="244" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="244" tag="" canDelete="false" writeOutText="false" classfield_id="244" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="247" parent_class="247" fileExt=".cpp" fileName="Monitor" >
+     <textblocks>
+      <codeblockwithcomments tag="includes" text="#include &quot;monitor.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="constructionMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Constructors/Destructors" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+      <hierarchicalcodeblock tag="otherMethodsBlock" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" text="Methods" />
+       </header>
+       <textblocks/>
+      </hierarchicalcodeblock>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        Monitor.cpp - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.cpp&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:42:04&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="248" field_type="17" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="248" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text="FIX ME;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="248" tag="" canDelete="false" writeOutText="false" classfield_id="248" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="248" tag="" canDelete="false" writeOutText="false" classfield_id="248" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="248" tag="" canDelete="false" writeOutText="false" classfield_id="248" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="248" tag="" canDelete="false" writeOutText="false" classfield_id="248" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="248" tag="" canDelete="false" writeOutText="false" classfield_id="248" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader145" parent_class="145" fileExt=".h" fileName="Message" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef MESSAGE_H&amp;#010;#define MESSAGE_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;#include &quot;item.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="145" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class Message&amp;#010;Representation of an email message or a news article." />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="Message ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //MESSAGE_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        Message.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:40:56&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="163" field_type="140119768" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="163" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" Item ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="163" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="163" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="163" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="163" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="163" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="163" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="163" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="163" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="163" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="163" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader146" parent_class="146" fileExt=".h" fileName="MessageModel" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef MESSAGEMODEL_H&amp;#010;#define MESSAGEMODEL_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;#include &quot;job.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="146" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class MessageModel&amp;#010; A flat self-updating message model.&amp;#010;" />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="MessageModel ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //MESSAGEMODEL_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        MessageModel.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:26:33&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="236" field_type="7536745" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="236" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" Job ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="236" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="236" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="236" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="236" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="236" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="236" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="236" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="236" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="236" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="236" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader147" parent_class="147" fileExt=".h" fileName="MessageFetchJob" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef MESSAGEFETCHJOB_H&amp;#010;#define MESSAGEFETCHJOB_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;#include &quot;job.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="147" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class MessageFetchJob&amp;#010;Fetches message data from the backend." />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks>
+              <ccfdeclarationcodeblock parent_id="166" tag="tblock_0" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" Job ;" >
+               <header>
+                <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+               </header>
+              </ccfdeclarationcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="MessageFetchJob ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks>
+                  <codeaccessormethod accessType="0" parent_id="166" tag="hblock_tag_0" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="166" role_id="0" >
+                   <header>
+                    <cppcodedocumentation tag="" indentLevel="1" />
+                   </header>
+                  </codeaccessormethod>
+                  <codeaccessormethod accessType="1" parent_id="166" tag="hblock_tag_1" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="166" role_id="0" >
+                   <header>
+                    <cppcodedocumentation tag="" indentLevel="1" />
+                   </header>
+                  </codeaccessormethod>
+                  <codeaccessormethod accessType="2" parent_id="166" tag="hblock_tag_2" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="166" role_id="0" >
+                   <header>
+                    <cppcodedocumentation tag="" indentLevel="1" />
+                   </header>
+                  </codeaccessormethod>
+                  <codeaccessormethod accessType="3" parent_id="166" tag="hblock_tag_3" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="166" role_id="0" >
+                   <header>
+                    <cppcodedocumentation tag="" indentLevel="1" />
+                   </header>
+                  </codeaccessormethod>
+                  <codeaccessormethod accessType="4" parent_id="166" tag="hblock_tag_4" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="166" role_id="0" >
+                   <header>
+                    <cppcodedocumentation tag="" indentLevel="1" />
+                   </header>
+                  </codeaccessormethod>
+                 </textblocks>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //MESSAGEFETCHJOB_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        MessageFetchJob.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:11:38&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="166" field_type="0" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="166" tag="tblock_0" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" Job ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="166" tag="hblock_tag_0" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="166" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="166" tag="hblock_tag_1" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="166" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="166" tag="hblock_tag_2" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="166" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="166" tag="hblock_tag_3" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="166" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="166" tag="hblock_tag_4" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="166" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader148" parent_class="148" fileExt=".h" fileName="MessageQuery" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef MESSAGEQUERY_H&amp;#010;#define MESSAGEQUERY_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;#include &quot;job.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="148" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class MessageQuery&amp;#010;Retrieving of (partial) messages matching a given query pattern." />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks>
+              <ccfdeclarationcodeblock parent_id="195" tag="tblock_0" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" Job ;" >
+               <header>
+                <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+               </header>
+              </ccfdeclarationcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="MessageQuery ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks>
+                  <codeaccessormethod accessType="0" parent_id="195" tag="hblock_tag_0" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="195" role_id="0" >
+                   <header>
+                    <cppcodedocumentation tag="" indentLevel="1" />
+                   </header>
+                  </codeaccessormethod>
+                  <codeaccessormethod accessType="1" parent_id="195" tag="hblock_tag_1" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="195" role_id="0" >
+                   <header>
+                    <cppcodedocumentation tag="" indentLevel="1" />
+                   </header>
+                  </codeaccessormethod>
+                  <codeaccessormethod accessType="2" parent_id="195" tag="hblock_tag_2" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="195" role_id="0" >
+                   <header>
+                    <cppcodedocumentation tag="" indentLevel="1" />
+                   </header>
+                  </codeaccessormethod>
+                  <codeaccessormethod accessType="3" parent_id="195" tag="hblock_tag_3" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="195" role_id="0" >
+                   <header>
+                    <cppcodedocumentation tag="" indentLevel="1" />
+                   </header>
+                  </codeaccessormethod>
+                  <codeaccessormethod accessType="4" parent_id="195" tag="hblock_tag_4" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="195" role_id="0" >
+                   <header>
+                    <cppcodedocumentation tag="" indentLevel="1" />
+                   </header>
+                  </codeaccessormethod>
+                 </textblocks>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //MESSAGEQUERY_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        MessageQuery.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:28:16&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="195" field_type="0" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="195" tag="tblock_0" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" Job ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="195" tag="hblock_tag_0" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="195" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="195" tag="hblock_tag_1" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="195" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="195" tag="hblock_tag_2" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="195" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="195" tag="hblock_tag_3" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="195" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="195" tag="hblock_tag_4" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="195" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader150" parent_class="150" fileExt=".h" fileName="Item" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef ITEM_H&amp;#010;#define ITEM_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="150" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class Item&amp;#010;Base class for all PIM items stored in Akonadi.&amp;#010;  It contains type-neutral data and the unique reference.&amp;#010;" />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks>
+              <ccfdeclarationcodeblock parent_id="163" tag="tblock_0" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" Message ;" >
+               <header>
+                <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+               </header>
+              </ccfdeclarationcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="Item ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks>
+                  <codeaccessormethod accessType="0" parent_id="163" tag="hblock_tag_0" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="163" role_id="1" >
+                   <header>
+                    <cppcodedocumentation tag="" indentLevel="1" />
+                   </header>
+                  </codeaccessormethod>
+                  <codeaccessormethod accessType="1" parent_id="163" tag="hblock_tag_1" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="163" role_id="1" >
+                   <header>
+                    <cppcodedocumentation tag="" indentLevel="1" />
+                   </header>
+                  </codeaccessormethod>
+                  <codeaccessormethod accessType="2" parent_id="163" tag="hblock_tag_2" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="163" role_id="1" >
+                   <header>
+                    <cppcodedocumentation tag="" indentLevel="1" />
+                   </header>
+                  </codeaccessormethod>
+                  <codeaccessormethod accessType="3" parent_id="163" tag="hblock_tag_3" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="163" role_id="1" >
+                   <header>
+                    <cppcodedocumentation tag="" indentLevel="1" />
+                   </header>
+                  </codeaccessormethod>
+                  <codeaccessormethod accessType="4" parent_id="163" tag="hblock_tag_4" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="163" role_id="1" >
+                   <header>
+                    <cppcodedocumentation tag="" indentLevel="1" />
+                   </header>
+                  </codeaccessormethod>
+                 </textblocks>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //ITEM_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        Item.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:40:53&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="163" field_type="0" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="163" tag="tblock_0" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" Message ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="163" tag="hblock_tag_0" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="163" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="163" tag="hblock_tag_1" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="163" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="163" tag="hblock_tag_2" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="163" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="163" tag="hblock_tag_3" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="163" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="163" tag="hblock_tag_4" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="163" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader155" parent_class="155" fileExt=".h" fileName="Job" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef JOB_H&amp;#010;#define JOB_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="155" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class Job&amp;#010;  This class encapsulates a request to the pim storage service,&amp;#010;  the code looks like&amp;#010;&amp;#010;  \code&amp;#010;    PIM::Job *job = new PIM::SomeJob( some parameter );&amp;#010;    connect( job, SIGNAL( done( PIM::Job* ) ),&amp;#010;             this, SLOT( slotResult( PIM::Job* ) ) );&amp;#010;    job->start();&amp;#010;  \endcode&amp;#010;&amp;#010;  And the slotResult is usually at least:&amp;#010;&amp;#010;  \code&amp;#010;    if ( job->error() )&amp;#010;      // handle error...&amp;#010;  \endcode&amp;#010;&amp;#010;  With the synchronous interface the code looks like&amp;#010;&amp;#010;  \code&amp;#010;    PIM::SomeJob job( some parameter );&amp;#010;    if ( !job.exec() ) {&amp;#010;      qDebug( &quot;Error: %s&quot;, qPrintable( job.errorString() ) );&amp;#010;    } else {&amp;#010;      // do something&amp;#010;    }&amp;#010;  \endcode&amp;#010;&amp;#010;  Subclasses must reimplement @see doStart().&amp;#010;" />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="Job ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //JOB_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        Job.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:26:02&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="166" field_type="7143521" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="166" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" MessageFetchJob ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="166" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="166" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="166" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="166" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="166" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="166" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="166" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="166" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="166" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="166" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="180" field_type="6619252" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="180" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" CollectionCreateJob ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="180" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="180" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="180" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="180" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="180" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="180" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="180" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="180" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="180" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="180" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="184" field_type="25" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="184" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" CollectionRenameJob ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="184" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="184" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="184" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="184" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="184" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="184" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="184" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="184" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="184" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="184" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="188" field_type="7602275" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="188" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" CollectionSelectJob ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="188" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="188" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="188" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="188" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="188" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="188" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="188" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="188" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="188" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="188" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="192" field_type="6881388" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="192" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" ItemAppendJob ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="192" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="192" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="192" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="192" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="192" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="192" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="192" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="192" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="192" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="192" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="195" field_type="7536745" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="195" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" MessageQuery ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="195" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="195" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="195" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="195" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="195" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="195" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="195" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="195" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="195" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="195" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="200" field_type="7602291" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="200" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" DataRequest ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="200" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="200" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="200" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="200" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="200" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="200" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="200" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="200" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="200" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="200" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="207" field_type="655370" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="207" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" Query ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="207" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="207" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="207" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="207" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="207" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="207" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="207" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="207" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="207" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="207" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="211" field_type="7274570" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="211" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" CollectionListJob ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="211" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="211" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="211" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="211" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="211" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="211" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="211" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="211" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="211" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="211" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="222" field_type="2097192" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="222" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" JobQueue ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="222" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="222" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="222" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="222" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="222" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="222" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="222" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="222" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="222" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="222" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="236" field_type="7536745" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="236" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" MessageModel ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="236" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="236" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="236" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="236" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="236" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="236" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="236" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="236" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="236" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="236" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader169" parent_class="169" fileExt=".h" fileName="Collection" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef COLLECTION_H&amp;#010;#define COLLECTION_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;#include &quot;collectionattribute.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="169" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class Collection&amp;#010; This class represents a collection of PIM objects, such as a folder on a mail- or&amp;#010;  groupware-server.&amp;#010;&amp;#010;  Collections are hierarchical, i.e. they may have a parent collectio" />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="Collection ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //COLLECTION_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        Collection.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:43:16&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="171" field_type="7274601" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="171" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" CollectionAttribute ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="171" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="171" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="171" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="171" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="171" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="171" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="171" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="171" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="171" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="171" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="174" field_type="7274601" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="174" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" CollectionAttribute ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="174" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="174" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="174" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="174" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="174" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="174" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="174" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="174" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="174" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="174" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader170" parent_class="170" fileExt=".h" fileName="CollectionAttribute" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef COLLECTIONATTRIBUTE_H&amp;#010;#define COLLECTIONATTRIBUTE_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="170" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class CollectionAttribute&amp;#010;" />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="CollectionAttribute ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //COLLECTIONATTRIBUTE_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        CollectionAttribute.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:43:08&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="171" field_type="6226036" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="171" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" Collection ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="171" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="171" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="171" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="171" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="171" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="171" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="171" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="171" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="171" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="171" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="174" field_type="6226036" initialValue=" new vector( )" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="174" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" vector Vector;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="174" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="174" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="174" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="174" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="174" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="174" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="174" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="174" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="174" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="174" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="230" field_type="6226036" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="230" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" MessageCollectionAttribute ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="230" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="230" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="230" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="230" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="230" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="230" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="230" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="230" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="230" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="230" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader179" parent_class="179" fileExt=".h" fileName="CollectionCreateJob" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef COLLECTIONCREATEJOB_H&amp;#010;#define COLLECTIONCREATEJOB_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;#include &quot;job.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="179" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class CollectionCreateJob&amp;#010;Job to create collections." />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="CollectionCreateJob ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //COLLECTIONCREATEJOB_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        CollectionCreateJob.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:12:25&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="180" field_type="6226036" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="180" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" Job ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="180" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="180" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="180" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="180" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="180" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="180" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="180" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="180" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="180" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="180" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader183" parent_class="183" fileExt=".h" fileName="CollectionRenameJob" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef COLLECTIONRENAMEJOB_H&amp;#010;#define COLLECTIONRENAMEJOB_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;#include &quot;job.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="183" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class CollectionRenameJob&amp;#010;Job to rename/move a collection." />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="CollectionRenameJob ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //COLLECTIONRENAMEJOB_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        CollectionRenameJob.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:11:27&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="184" field_type="49" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="184" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" Job ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="184" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="184" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="184" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="184" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="184" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="184" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="184" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="184" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="184" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="184" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader187" parent_class="187" fileExt=".h" fileName="CollectionSelectJob" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef COLLECTIONSELECTJOB_H&amp;#010;#define COLLECTIONSELECTJOB_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;#include &quot;job.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="187" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class CollectionSelectJob&amp;#010;Selects a specific collection. See RFC 3501 for select semantics." />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="CollectionSelectJob ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //COLLECTIONSELECTJOB_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        CollectionSelectJob.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:11:22&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="188" field_type="6226036" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="188" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" Job ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="188" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="188" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="188" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="188" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="188" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="188" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="188" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="188" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="188" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="188" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader191" parent_class="191" fileExt=".h" fileName="ItemAppendJob" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef ITEMAPPENDJOB_H&amp;#010;#define ITEMAPPENDJOB_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;#include &quot;job.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="191" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class ItemAppendJob&amp;#010;Creates a new PIM item on the backend.&amp;#010;Can be used as base class for type specific append jobs.&amp;#010;" />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="ItemAppendJob ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //ITEMAPPENDJOB_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        ItemAppendJob.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:12:35&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="192" field_type="6881388" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="192" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" Job ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="192" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="192" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="192" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="192" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="192" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="192" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="192" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="192" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="192" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="192" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader198" parent_class="198" fileExt=".h" fileName="DataRequest" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef DATAREQUEST_H&amp;#010;#define DATAREQUEST_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;#include &quot;job.h&quot;&amp;#010;#include &quot;datareference.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="198" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class DataRequest&amp;#010; This class requests the actual data from the pim storage&amp;#010;  service for a list of @see DataReferences. The request can&amp;#010;  be parameterized by the part of the data your are interested in&amp;#010;  and the acceptable mimetypes.&amp;#010;&amp;#010;  Example:&amp;#010;&amp;#010;  \code&amp;#010;    SomeDataRequest request( references, &quot;ALL&quot;, QStringList( &quot;text/x-&amp;#010;vcard&quot; ) );&amp;#010;    if ( !request.exec() ) {&amp;#010;      qDebug( &quot;Error: %s&quot;, qPrintable( request.errorString() ) );&amp;#010;      return;&amp;#010;    }&amp;#010;&amp;#010;    for ( int i = 0; i &lt; request.count(); ++i ) {&amp;#010;      if ( request.mimetype( i ) == &quot;text/x-vcard&quot; ) {&amp;#010;        doSomeConvertion( request.data( i ) );&amp;#010;      }&amp;#010;    }&amp;#010;  \endcode&amp;#010; " />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="DataRequest ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //DATAREQUEST_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        DataRequest.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:12:24&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="200" field_type="7602291" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="200" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" Job ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="200" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="200" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="200" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="200" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="200" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="200" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="200" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="200" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="200" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="200" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="203" field_type="6619238" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="203" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" DataReference ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="203" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="203" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="203" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="203" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="203" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="203" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="203" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="203" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="203" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="203" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader199" parent_class="199" fileExt=".h" fileName="DataReference" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef DATAREFERENCE_H&amp;#010;#define DATAREFERENCE_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="199" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class DataReference&amp;#010;  This class encapsulates a reference to a pim object in&amp;#010;  the pim storage service.&amp;#010; " />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="DataReference ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //DATAREFERENCE_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        DataReference.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:42:04&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="203" field_type="6619218" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="203" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" DataRequest ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="203" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="203" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="203" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="203" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="203" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="203" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="203" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="203" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="203" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="203" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="248" field_type="7274612" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="248" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" Monitor ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="248" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="248" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="248" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="248" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="248" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="248" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="248" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="248" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="248" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="248" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader206" parent_class="206" fileExt=".h" fileName="Query" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef QUERY_H&amp;#010;#define QUERY_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;#include &quot;job.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="206" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class Query&amp;#010; This class queries the pim storage service for a list of&amp;#010;  @see DataReferences, which match a given query pattern.&amp;#010;&amp;#010;  Example for synchronous API:&amp;#010;&amp;#010;  \code&amp;#010;    Query query;&amp;#010;    query.setQueryPattern( &quot;mimetype=message/rfc822; folder=/inbox; d&amp;#010;ate>=2005-12-31;&quot; );&amp;#010;    query.exec();&amp;#010;    const QList&lt;PIM::DataReference> result = query.result();&amp;#010;  \endcode&amp;#010; */&amp;#010;" />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="Query ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //QUERY_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        Query.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:37:00&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="207" field_type="655370" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="207" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" Job ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="207" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="207" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="207" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="207" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="207" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="207" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="207" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="207" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="207" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="207" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="240" field_type="7536745" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="240" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" EmailQuery ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="240" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="240" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="240" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="240" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="240" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="240" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="240" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="240" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="240" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="240" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="244" field_type="7077983" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="244" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" ContactQuery ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="244" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="244" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="244" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="244" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="244" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="244" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="244" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="244" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="244" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="244" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader210" parent_class="210" fileExt=".h" fileName="CollectionListJob" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef COLLECTIONLISTJOB_H&amp;#010;#define COLLECTIONLISTJOB_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;#include &quot;job.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="210" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class CollectionListJob&amp;#010;  This class can be used to retrieve the complete or partial collecti&amp;#010;on tree&amp;#010;  of the PIM storage service.&amp;#010;&amp;#010;  It returns a QHash of references to PIM::Collection objects.&amp;#010;&amp;#010;  @todo Add partial collection retrieval (eg. only collections contai&amp;#010;ning contacts).&amp;#010;*" />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="CollectionListJob ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //COLLECTIONLISTJOB_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        CollectionListJob.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 16:54:59&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="211" field_type="7274570" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="211" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" Job ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="211" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="211" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="211" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="211" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="211" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="211" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="211" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="211" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="211" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="211" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader214" parent_class="214" fileExt=".h" fileName="CollectionModel" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef COLLECTIONMODEL_H&amp;#010;#define COLLECTIONMODEL_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="214" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class CollectionModel&amp;#010;  Model to handle a collection tree.&amp;#010;&amp;#010;  @todo Add support for collection filtering, eg. deal only with collections&amp;#010;  containing contacts.&amp;#010;&amp;#010;  @todo Split into generic and KDE dependent parts?&amp;#010;" />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="CollectionModel ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //COLLECTIONMODEL_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        CollectionModel.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:42:57&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="216" field_type="7077996" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="216" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" CollectionView ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="216" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="216" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="216" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="216" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="216" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="216" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="216" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="216" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="216" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="216" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="226" field_type="7536755" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="226" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" MessageCollectionModel ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="226" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="226" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="226" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="226" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="226" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="226" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="226" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="226" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="226" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="226" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader215" parent_class="215" fileExt=".h" fileName="CollectionView" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef COLLECTIONVIEW_H&amp;#010;#define COLLECTIONVIEW_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;#include &quot;collectionmodel.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="215" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class CollectionView&amp;#010;" />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="CollectionView ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //COLLECTIONVIEW_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        CollectionView.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:42:48&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="216" field_type="6619244" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="216" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" CollectionModel ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="216" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="216" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="216" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="216" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="216" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="216" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="216" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="216" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="216" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="216" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader219" parent_class="219" fileExt=".h" fileName="ImapParser" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef IMAPPARSER_H&amp;#010;#define IMAPPARSER_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="219" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class ImapParser&amp;#010;  Helper functions to parse IMAP responses.&amp;#010;  @todo Not really Akonadi specific, move somewhere else.&amp;#010;" />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="ImapParser ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //IMAPPARSER_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        ImapParser.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:04:38&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields/>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader221" parent_class="221" fileExt=".h" fileName="JobQueue" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef JOBQUEUE_H&amp;#010;#define JOBQUEUE_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;#include &quot;job.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="221" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class JobQueue&amp;#010; A job queue. All jobs you put in here are executed sequentially. JobQueue can&amp;#010;  be used as a parent for jobs to share the same connection to the Akonadi backend. Do not start enqueued jobs manually!&amp;#010;" />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="JobQueue ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //JOBQUEUE_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        JobQueue.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:27:18&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="222" field_type="2097192" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="222" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" Job ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="222" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="222" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="222" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="222" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="222" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="222" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="222" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="222" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="222" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="222" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader225" parent_class="225" fileExt=".h" fileName="MessageCollectionModel" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef MESSAGECOLLECTIONMODEL_H&amp;#010;#define MESSAGECOLLECTIONMODEL_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;#include &quot;collectionmodel.h&quot;&amp;#010;#include &quot;messagecollectionattribute.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="225" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class MessageCollectionModel&amp;#010;f  Extended modell for message collections.&amp;#010;  Supports columns for message unread/total counts.&amp;#010;" />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="MessageCollectionModel ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //MESSAGECOLLECTIONMODEL_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        MessageCollectionModel.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:42:54&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="226" field_type="6750266" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="226" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" CollectionModel ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="226" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="226" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="226" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="226" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="226" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="226" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="226" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="226" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="226" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="226" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="233" field_type="6815860" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="233" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" MessageCollectionAttribute ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="233" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="233" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="233" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="233" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="233" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="233" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="233" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="233" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="233" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="233" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader229" parent_class="229" fileExt=".h" fileName="MessageCollectionAttribute" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef MESSAGECOLLECTIONATTRIBUTE_H&amp;#010;#define MESSAGECOLLECTIONATTRIBUTE_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;#include &quot;collectionattribute.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="229" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class MessageCollectionAttribute&amp;#010;  A collection of messages, eg. emails or news articles.&amp;#010;" />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="MessageCollectionAttribute ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //MESSAGECOLLECTIONATTRIBUTE_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        MessageCollectionAttribute.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:43:06&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="230" field_type="3801189" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="230" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" CollectionAttribute ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="230" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="230" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="230" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="230" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="230" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="230" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="230" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="230" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="230" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="230" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+      <codeclassfield parent_id="233" field_type="6815860" initialValue="" role_id="0" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="233" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="1" text=" MessageCollectionModel ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="233" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="233" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="233" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="233" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="233" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="233" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="233" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="233" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="233" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="233" role_id="1" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader239" parent_class="239" fileExt=".h" fileName="EmailQuery" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef EMAILQUERY_H&amp;#010;#define EMAILQUERY_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;#include &quot;query.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="239" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class EmailQuery&amp;#010;Query for mails" />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="EmailQuery ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //EMAILQUERY_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        EmailQuery.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:39:18&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="240" field_type="7536745" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="240" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" Query ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="240" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="240" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="240" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="240" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="240" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="240" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="240" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="240" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="240" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="240" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader243" parent_class="243" fileExt=".h" fileName="ContactQuery" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef CONTACTQUERY_H&amp;#010;#define CONTACTQUERY_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;#include &quot;query.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="243" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class ContactQuery&amp;#010;" />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="ContactQuery ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //CONTACTQUERY_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        ContactQuery.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:36:39&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="244" field_type="7077983" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="244" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" Query ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="244" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="244" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="244" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="244" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="244" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="244" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="244" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="244" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="244" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="244" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+    <classifiercodedocument writeOutCode="true" package="-1" id="cppheader247" parent_class="247" fileExt=".h" fileName="Monitor" >
+     <textblocks>
+      <codeblockwithcomments tag="hashDefBlock" text="#ifndef MONITOR_H&amp;#010;#define MONITOR_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="includes" text="#include &lt;string>&amp;#010;#include &quot;datareference.h&quot;&amp;#010;" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <codeblockwithcomments tag="using" writeOutText="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+      <hierarchicalcodeblock tag="namespace" canDelete="false" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" text="Namespace" />
+       </header>
+       <textblocks>
+        <codeblockwithcomments tag="enums" writeOutText="false" >
+         <header>
+          <cppcodedocumentation tag="" writeOutText="false" />
+         </header>
+        </codeblockwithcomments>
+        <cppheaderclassdeclarationblock parent_id="247" tag="classDeclarationBlock" canDelete="false" >
+         <header>
+          <cppcodedocumentation tag="" text="Class Monitor&amp;#010;Monitors an Item or Collection for changes and emits signals if som&amp;#010;e&amp;#010;  of these objects are changed or removed or new ones are added to th&amp;#010;e storage&amp;#010;  backend.&amp;#010;&amp;#010;  @todo: support un-monitoring&amp;#010;  @todo: distinguish between monitoring collection properties and col&amp;#010;lection content.&amp;#010;" />
+         </header>
+         <textblocks>
+          <hierarchicalcodeblock tag="publicBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Public stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="publicFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="pubMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks>
+                <codeblockwithcomments tag="emptyconstructor" writeOutText="false" indentLevel="1" text="Monitor ( ) { }" >
+                 <header>
+                  <cppcodedocumentation tag="" indentLevel="1" text="Empty Constructor" />
+                 </header>
+                </codeblockwithcomments>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="pubStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="pubRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="protectedBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Protected stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="protectedFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="protMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="protStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="protRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+          <hierarchicalcodeblock tag="privateBlock" canDelete="false" >
+           <header>
+            <cppcodedocumentation tag="" text="Private stuff" />
+           </header>
+           <textblocks>
+            <hierarchicalcodeblock tag="privateFieldsDecl" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" text="Fields" />
+             </header>
+             <textblocks/>
+            </hierarchicalcodeblock>
+            <hierarchicalcodeblock tag="privMethodsBlock" canDelete="false" indentLevel="1" >
+             <header>
+              <cppcodedocumentation tag="" indentLevel="1" />
+             </header>
+             <textblocks>
+              <hierarchicalcodeblock tag="constructionMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Constructors" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="accessorMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Accessor Methods" />
+               </header>
+               <textblocks>
+                <hierarchicalcodeblock tag="privStaticAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+                <hierarchicalcodeblock tag="privRegularAccessorMethods" canDelete="false" indentLevel="1" >
+                 <header>
+                  <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+                 </header>
+                 <textblocks/>
+                </hierarchicalcodeblock>
+               </textblocks>
+              </hierarchicalcodeblock>
+              <hierarchicalcodeblock tag="operationMethods" canDelete="false" indentLevel="1" >
+               <header>
+                <cppcodedocumentation tag="" indentLevel="1" text="Operations" />
+               </header>
+               <textblocks/>
+              </hierarchicalcodeblock>
+             </textblocks>
+            </hierarchicalcodeblock>
+           </textblocks>
+          </hierarchicalcodeblock>
+         </textblocks>
+        </cppheaderclassdeclarationblock>
+       </textblocks>
+      </hierarchicalcodeblock>
+      <codeblockwithcomments tag="hashDefBlockEnd" text="#endif //MONITOR_H" >
+       <header>
+        <cppcodedocumentation tag="" writeOutText="false" />
+       </header>
+      </codeblockwithcomments>
+     </textblocks>
+     <header>
+      <codecomment tag="" text="/************************************************************************&amp;#010;                        Monitor.h - Copyright will&amp;#010;&amp;#010;Here you can write a license for your code, some comments or any other&amp;#010;information you want to have in your generated code. To to this simply&amp;#010;configure the &quot;headings&quot; directory in uml to point to a directory&amp;#010;where you have your heading files.&amp;#010;&amp;#010;or you can just replace the contents of this file with your own.&amp;#010;If you want to do this, this file is located at&amp;#010;&amp;#010;/opt/kde3/share/apps/umbrello/headings/heading.h&amp;#010;&amp;#010;-->Code Generators searches for heading files based on the file extension&amp;#010;   i.e. it will look for a file name ending in &quot;.h&quot; to include in C++ header&amp;#010;   files, and for a file name ending in &quot;.java&quot; to include in all generated&amp;#010;   java code.&amp;#010;   If you name the file &quot;heading.&lt;extension>&quot;, Code Generator will always&amp;#010;   choose this file even if there are other files with the same extension in the&amp;#010;   directory. If you name the file something else, it must be the only one with that&amp;#010;   extension in the directory to guarantee that Code Generator will choose it.&amp;#010;&amp;#010;you can use variables in your heading files which are replaced at generation&amp;#010;time. possible variables are : author, date, time, filename and filepath.&amp;#010;just write %variable_name%&amp;#010;&amp;#010;This file was generated on Wed Jul 5 2006 at 17:42:04&amp;#010;The original location of this file is &amp;#010;**************************************************************************/&amp;#010;" />
+     </header>
+     <classfields>
+      <codeclassfield parent_id="248" field_type="6488174" initialValue="" role_id="1" writeOutMethods="true" listClassName="" >
+       <header>
+        <cppcodedocumentation tag="" />
+       </header>
+       <ccfdeclarationcodeblock parent_id="248" tag="" canDelete="false" writeOutText="false" indentLevel="1" role_id="0" text=" DataReference ;" >
+        <header>
+         <cppcodedocumentation tag="" writeOutText="false" indentLevel="1" />
+        </header>
+       </ccfdeclarationcodeblock>
+       <codeaccessormethod accessType="0" parent_id="248" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="248" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="1" parent_id="248" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="248" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="2" parent_id="248" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="248" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="3" parent_id="248" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="248" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+       <codeaccessormethod accessType="4" parent_id="248" tag="" canDelete="false" writeOutText="false" indentLevel="1" classfield_id="248" role_id="0" >
+        <header>
+         <cppcodedocumentation tag="" indentLevel="1" />
+        </header>
+       </codeaccessormethod>
+      </codeclassfield>
+     </classfields>
+    </classifiercodedocument>
+   </codegenerator>
+  </codegeneration>
+ </XMI.extensions>
+</XMI>
diff --git a/doc/pics/akonadi_agent_handling.eps b/doc/pics/akonadi_agent_handling.eps
new file mode 100644 (file)
index 0000000..974edc1
--- /dev/null
@@ -0,0 +1,1142 @@
+%!PS-Adobe-1.0 EPSF-3.0
+%%BoundingBox: 35 656 610 819
+%%Creator: Qt 3.3.6
+%%CreationDate: Do Sep 28 16:06:56 2006
+%%Orientation: Portrait
+%%Pages: 1
+%%DocumentFonts: BitstreamVeraSerif-Roman
+
+%%EndComments
+%%BeginProlog
+% Prolog copyright 1994-2005 Trolltech. You may copy this prolog in any way
+% that is directly related to this document. For other use of this prolog,
+% see your licensing agreement for Qt.
+/d/def load def/D{bind d}bind d/d2{dup dup}D/B{0 d2}D/W{255 d2}D/ED{exch d}D
+/D0{0 ED}D/LT{lineto}D/MT{moveto}D/S{stroke}D/F{setfont}D/SW{setlinewidth}D
+/CP{closepath}D/RL{rlineto}D/NP{newpath}D/CM{currentmatrix}D/SM{setmatrix}D
+/TR{translate}D/SD{setdash}D/SC{aload pop setrgbcolor}D/CR{currentfile read
+pop}D/i{index}D/bs{bitshift}D/scs{setcolorspace}D/DB{dict dup begin}D/DE{end
+d}D/ie{ifelse}D/sp{astore pop}D/BSt 0 d/LWi 1 d/PSt 1 d/Cx 0 d/Cy 0 d/WFi
+false d/OMo false d/BCol[1 1 1]d/PCol[0 0 0]d/BkCol[1 1 1]d/BDArr[0.94 0.88
+0.63 0.50 0.37 0.12 0.06]d/defM matrix d/nS 0 d/GPS{PSt 1 ge PSt 5 le and{{
+LArr PSt 1 sub 2 mul get}{LArr PSt 2 mul 1 sub get}ie}{[]}ie}D/QS{PSt 0 ne{
+gsave LWi SW true GPS 0 SD S OMo PSt 1 ne and{BkCol SC false GPS dup 0 get
+SD S}if grestore}if}D/r28{{CR dup 32 gt{exit}if pop}loop 3{CR}repeat 0 4{7
+bs exch dup 128 gt{84 sub}if 42 sub 127 and add}repeat}D/rA 0 d/rL 0 d/rB{rL
+0 eq{/rA r28 d/rL 28 d}if dup rL gt{rA exch rL sub rL exch/rA 0 d/rL 0 d rB
+exch bs add}{dup rA 16#fffffff 3 -1 roll bs not and exch dup rL exch sub/rL
+ED neg rA exch bs/rA ED}ie}D/uc{/rL 0 d 0{dup 2 i length ge{exit}if 1 rB 1
+eq{3 rB dup 3 ge{1 add dup rB 1 i 5 ge{1 i 6 ge{1 i 7 ge{1 i 8 ge{128 add}if
+64 add}if 32 add}if 16 add}if 3 add exch pop}if 3 add exch 10 rB 1 add{dup 3
+i lt{dup}{2 i}ie 4 i 3 i 3 i sub 2 i getinterval 5 i 4 i 3 -1 roll
+putinterval dup 4 -1 roll add 3 1 roll 4 -1 roll exch sub dup 0 eq{exit}if 3
+1 roll}loop pop pop}{3 rB 1 add{2 copy 8 rB put 1 add}repeat}ie}loop pop}D
+/sl D0/QCIgray D0/QCIcolor D0/QCIindex D0/QCI{/colorimage where{pop false 3
+colorimage}{exec/QCIcolor ED/QCIgray QCIcolor length 3 idiv string d 0 1
+QCIcolor length 3 idiv 1 sub{/QCIindex ED/x QCIindex 3 mul d QCIgray
+QCIindex QCIcolor x get 0.30 mul QCIcolor x 1 add get 0.59 mul QCIcolor x 2
+add get 0.11 mul add add cvi put}for QCIgray image}ie}D/di{gsave TR 1 i 1 eq
+{false eq{pop true 3 1 roll 4 i 4 i false 4 i 4 i imagemask BkCol SC
+imagemask}{pop false 3 1 roll imagemask}ie}{dup false ne{/languagelevel
+where{pop languagelevel 3 ge}{false}ie}{false}ie{/ma ED 8 eq{/dc[0 1]d
+/DeviceGray}{/dc[0 1 0 1 0 1]d/DeviceRGB}ie scs/im ED/mt ED/h ED/w ED/id 7
+DB/ImageType 1 d/Width w d/Height h d/ImageMatrix mt d/DataSource im d
+/BitsPerComponent 8 d/Decode dc d DE/md 7 DB/ImageType 1 d/Width w d/Height
+h d/ImageMatrix mt d/DataSource ma d/BitsPerComponent 1 d/Decode[0 1]d DE 4
+DB/ImageType 3 d/DataDict id d/MaskDict md d/InterleaveType 3 d end image}{
+pop 8 4 1 roll 8 eq{image}{QCI}ie}ie}ie grestore}d/BF{gsave BSt 1 eq{BCol SC
+WFi{fill}{eofill}ie}if BSt 2 ge BSt 8 le and{BDArr BSt 2 sub get/sc ED BCol{
+1. exch sub sc mul 1. exch sub}forall 3 array astore SC WFi{fill}{eofill}ie}
+if BSt 9 ge BSt 14 le and{WFi{clip}{eoclip}ie defM SM pathbbox 3 i 3 i TR 4
+2 roll 3 2 roll exch sub/h ED sub/w ED OMo{NP 0 0 MT 0 h RL w 0 RL 0 h neg
+RL CP BkCol SC fill}if BCol SC 0.3 SW NP BSt 9 eq BSt 11 eq or{0 4 h{dup 0
+exch MT w exch LT}for}if BSt 10 eq BSt 11 eq or{0 4 w{dup 0 MT h LT}for}if
+BSt 12 eq BSt 14 eq or{w h gt{0 6 w h add{dup 0 MT h sub h LT}for}{0 6 w h
+add{dup 0 exch MT w sub w exch LT}for}ie}if BSt 13 eq BSt 14 eq or{w h gt{0
+6 w h add{dup h MT h sub 0 LT}for}{0 6 w h add{dup w exch MT w sub 0 exch LT
+}for}ie}if S}if BSt 24 eq{}if grestore}D/mat matrix d/ang1 D0/ang2 D0/w D0/h
+D0/x D0/y D0/ARC{/ang2 ED/ang1 ED/h ED/w ED/y ED/x ED mat CM pop x w 2 div
+add y h 2 div add TR 1 h w div neg scale ang2 0 ge{0 0 w 2 div ang1 ang1
+ang2 add arc}{0 0 w 2 div ang1 ang1 ang2 add arcn}ie mat SM}D/C D0/P{NP MT
+0.5 0.5 rmoveto 0 -1 RL -1 0 RL 0 1 RL CP fill}D/M{/Cy ED/Cx ED}D/L{NP Cx Cy
+MT/Cy ED/Cx ED Cx Cy LT QS}D/DL{NP MT LT QS}D/HL{1 i DL}D/VL{2 i exch DL}D/R
+{/h ED/w ED/y ED/x ED NP x y MT 0 h RL w 0 RL 0 h neg RL CP BF QS}D/ACR{/h
+ED/w ED/y ED/x ED x y MT 0 h RL w 0 RL 0 h neg RL CP}D/xr D0/yr D0/rx D0/ry
+D0/rx2 D0/ry2 D0/RR{/yr ED/xr ED/h ED/w ED/y ED/x ED xr 0 le yr 0 le or{x y
+w h R}{xr 100 ge yr 100 ge or{x y w h E}{/rx xr w mul 200 div d/ry yr h mul
+200 div d/rx2 rx 2 mul d/ry2 ry 2 mul d NP x rx add y MT x y rx2 ry2 180 -90
+x y h add ry2 sub rx2 ry2 270 -90 x w add rx2 sub y h add ry2 sub rx2 ry2 0
+-90 x w add rx2 sub y rx2 ry2 90 -90 ARC ARC ARC ARC CP BF QS}ie}ie}D/E{/h
+ED/w ED/y ED/x ED mat CM pop x w 2 div add y h 2 div add TR 1 h w div scale
+NP 0 0 w 2 div 0 360 arc mat SM BF QS}D/A{16 div exch 16 div exch NP ARC QS}
+D/PIE{/ang2 ED/ang1 ED/h ED/w ED/y ED/x ED NP x w 2 div add y h 2 div add MT
+x y w h ang1 16 div ang2 16 div ARC CP BF QS}D/CH{16 div exch 16 div exch NP
+ARC CP BF QS}D/BZ{curveto QS}D/CRGB{255 div 3 1 roll 255 div 3 1 roll 255
+div 3 1 roll}D/BC{CRGB BkCol sp}D/BR{CRGB BCol sp/BSt ED}D/WB{1 W BR}D/NB{0
+B BR}D/PE{setlinejoin setlinecap CRGB PCol sp/LWi ED/PSt ED LWi 0 eq{0.25
+/LWi ED}if PCol SC}D/P1{1 0 5 2 roll 0 0 PE}D/ST{defM SM concat}D/MF{true
+exch true exch{exch pop exch pop dup 0 get dup findfont dup/FontName get 3
+-1 roll eq{exit}if}forall exch dup 1 get/fxscale ED 2 get/fslant ED exch
+/fencoding ED[fxscale 0 fslant 1 0 0]makefont fencoding false eq{}{dup
+maxlength dict begin{1 i/FID ne{def}{pop pop}ifelse}forall/Encoding
+fencoding d currentdict end}ie definefont pop}D/MFEmb{findfont dup length
+dict begin{1 i/FID ne{d}{pop pop}ifelse}forall/Encoding ED currentdict end
+definefont pop}D/DF{findfont/fs 3 -1 roll d[fs 0 0 fs -1 mul 0 0]makefont d}
+D/ty 0 d/Y{/ty ED}D/Tl{gsave SW NP 1 i exch MT 1 i 0 RL S grestore}D/XYT{ty
+MT/xyshow where{pop pop xyshow}{exch pop 1 i dup length 2 div exch
+stringwidth pop 3 -1 roll exch sub exch div exch 0 exch ashow}ie}D/AT{ty MT
+1 i dup length 2 div exch stringwidth pop 3 -1 roll exch sub exch div exch 0
+exch ashow}D/QI{/C save d pageinit/Cx 0 d/Cy 0 d/OMo false d}D/QP{C restore
+showpage}D/SPD{/setpagedevice where{1 DB 3 1 roll d end setpagedevice}{pop
+pop}ie}D/SV{BSt LWi PSt Cx Cy WFi OMo BCol PCol BkCol/nS nS 1 add d gsave}D
+/RS{nS 0 gt{grestore/BkCol ED/PCol ED/BCol ED/OMo ED/WFi ED/Cy ED/Cx ED/PSt
+ED/LWi ED/BSt ED/nS nS 1 sub d}if}D/CLSTART{/clipTmp matrix CM d defM SM NP}
+D/CLEND{clip NP clipTmp SM}D/CLO{grestore gsave defM SM}D
+
+/LArr[ [] [] [ 10.417 3.125 ] [ 3.125 10.417 ] [ 3.125 3.125 ] [ 3.125 3.125 ] [ 5.208 3.125 3.125 3.125 ] [ 3.125 5.208 3.125 3.125 ] [ 5.208 3.125 3.125 3.125 3.125 ] [ 3.125 5.208 3.125 3.125 3.125 3.125 ] ] d
+/pageinit {
+35.52 24 translate
+% 185*280mm (portrait)
+0 793.92 translate 0.96 -0.96 scale/defM matrix CM d } d
+%%EndProlog
+%%BeginSetup
+% Fonts and encodings used
+/BitstreamVeraSerif-RomanList [
+[ /BitstreamVeraSerif-Roman 1.0 0.0 ]
+  [ /BitstreamVeraSerif 1.0 0.0 ]
+  [ /Helvetica 0.988 0.000 ]
+] d
+%%BeginFont: Bitstream Vera Serif
+%!PS-Adobe-3.0 Resource-Font
+%%Copyright: Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved.
+%%Creator: Converted from TrueType by Qt
+25 dict begin
+/_d{bind def}bind def
+/_m{moveto}_d
+/_l{lineto}_d
+/_cl{closepath eofill}_d
+/_c{curveto}_d
+/_sc{7 -1 roll{setcachedevice}{pop pop pop pop pop pop}ifelse}_d
+/_e{exec}_d
+/FontName /BitstreamVeraSerif-Roman def
+/PaintType 0 def
+/FontMatrix[.001 0 0 .001 0 0]def
+/FontBBox[-182 -235 1287 928]def
+/FontType 3 def
+/Encoding StandardEncoding def
+/FontInfo 10 dict dup begin
+/FamilyName (Bitstream Vera Serif) def
+/FullName (Bitstream Vera Serif) def
+/Notice (Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a trademark of Bitstream, Inc.) def
+/Weight (Roman) def
+/Version (Release 1.10) def
+/ItalicAngle 0.0 def
+/isFixedPitch false def
+/UnderlinePosition -213 def
+/UnderlineThickness 133 def
+end readonly def
+/CharStrings 32 dict dup begin
+/.notdef{600 0 50 -176 550 705 _sc
+50 -176 _m
+50 705 _l
+550 705 _l
+550 -176 _l
+50 -176 _l
+106 -120 _m
+494 -120 _l
+494 649 _l
+106 649 _l
+106 -120 _l
+_cl}_d
+/space{318 0 0 0 0 0 _sc
+}_d
+/parenleft{390 0 79 -155 319 760 _sc
+319 -155 _m
+239 -119 179 -63 139 13 _c
+99 89 79 186 79 302 _c
+79 418 99 514 139 591 _c
+179 668 239 724 319 760 _c
+319 712 _l
+269 677 233 628 211 566 _c
+189 503 178 415 178 302 _c
+178 188 189 100 211 38 _c
+233 -24 269 -72 319 -107 _c
+319 -155 _l
+_cl}_d
+/parenright{390 0 71 -155 311 760 _sc
+71 -155 _m
+71 -107 _l
+121 -72 157 -24 179 38 _c
+201 100 212 188 212 302 _c
+212 415 201 503 179 566 _c
+157 628 121 677 71 712 _c
+71 760 _l
+150 724 210 668 250 591 _c
+290 514 311 418 311 302 _c
+311 186 290 89 250 13 _c
+210 -63 150 -119 71 -155 _c
+_cl}_d
+/colon{337 0 104 -13 234 434 _sc
+104 51 _m
+104 69 110 84 123 97 _c
+135 109 151 116 169 116 _c
+187 116 202 109 215 97 _c
+227 84 234 69 234 51 _c
+234 32 227 17 215 5 _c
+203 -7 187 -13 169 -13 _c
+150 -13 134 -7 122 5 _c
+110 17 104 32 104 51 _c
+104 369 _m
+104 387 110 402 123 415 _c
+135 427 151 434 169 434 _c
+187 434 203 427 215 415 _c
+227 403 234 387 234 369 _c
+234 350 227 334 215 322 _c
+203 310 187 304 169 304 _c
+151 304 135 310 123 323 _c
+110 335 104 351 104 369 _c
+_cl}_d
+/A{722 0 -5 0 732 729 _sc
+200 264 _m
+468 264 _l
+334 611 _l
+200 264 _l
+-5 0 _m
+-5 52 _l
+58 52 _l
+318 729 _l
+400 729 _l
+660 52 _l
+732 52 _l
+732 0 _l
+467 0 _l
+467 52 _l
+548 52 _l
+487 212 _l
+180 212 _l
+119 52 _l
+199 52 _l
+199 0 _l
+-5 0 _l
+_cl}_d
+/B{{735 0 55 0 674 729 _sc
+247 52 _m
+393 52 _l
+451 52 494 64 521 90 _c
+548 115 562 155 562 211 _c
+562 265 548 305 521 331 _c
+494 356 451 369 393 369 _c
+247 369 _l
+247 52 _l
+55 0 _m
+55 52 _l
+148 52 _l
+148 677 _l
+55 677 _l
+55 729 _l
+415 729 _l
+488 729 543 714 581 684 _c
+618 654 637 609 637 549 _c
+637 505 624 471 598 445 _c
+572 419 535 404 485 398 _c
+547 390 594 370 626 338 _c
+658 306 674 264 674 211 _c
+674 139 651 85 605 51 _c
+559 17 488 0 392 0 _c
+55 0 _l
+247 421 _m
+371 421 _l
+424 421 463 431 488 451 _c
+512 471 525 504 525 549 _c
+}_e{525 593 512 626 488 646 _c
+463 666 424 677 371 677 _c
+247 677 _l
+247 421 _l
+_cl}_e}_d
+/C{{765 0 56 -13 705 742 _sc
+705 193 _m
+683 125 647 73 597 39 _c
+546 4 482 -13 405 -13 _c
+357 -13 312 -5 272 11 _c
+231 27 195 50 164 82 _c
+127 118 100 160 82 206 _c
+64 252 56 305 56 364 _c
+56 477 88 568 154 638 _c
+219 707 305 742 413 742 _c
+453 742 495 736 540 726 _c
+584 716 633 700 685 679 _c
+685 511 _l
+630 511 _l
+618 572 593 617 557 646 _c
+521 675 470 690 405 690 _c
+327 690 268 662 228 607 _c
+188 551 168 470 168 364 _c
+168 257 188 176 228 121 _c
+268 65 327 38 405 38 _c
+459 38 503 51 539 77 _c
+574 103 599 141 615 193 _c
+705 193 _l
+_cl}_e}_d
+/D{802 0 55 0 744 729 _sc
+247 52 _m
+338 52 _l
+432 52 505 79 556 133 _c
+606 187 632 264 632 365 _c
+632 466 606 543 556 597 _c
+505 650 432 677 338 677 _c
+247 677 _l
+247 52 _l
+55 0 _m
+55 52 _l
+148 52 _l
+148 677 _l
+55 677 _l
+55 729 _l
+345 729 _l
+471 729 569 697 639 633 _c
+709 569 744 479 744 365 _c
+744 250 708 160 638 96 _c
+568 32 470 0 345 0 _c
+55 0 _l
+_cl}_d
+/I{395 0 55 0 340 729 _sc
+247 52 _m
+340 52 _l
+340 0 _l
+55 0 _l
+55 52 _l
+148 52 _l
+148 677 _l
+55 677 _l
+55 729 _l
+340 729 _l
+340 677 _l
+247 677 _l
+247 52 _l
+_cl}_d
+/M{1024 0 50 0 973 729 _sc
+55 0 _m
+55 52 _l
+148 52 _l
+148 677 _l
+50 677 _l
+50 729 _l
+262 729 _l
+518 210 _l
+774 729 _l
+973 729 _l
+973 677 _l
+876 677 _l
+876 52 _l
+969 52 _l
+969 0 _l
+684 0 _l
+684 52 _l
+777 52 _l
+777 615 _l
+527 107 _l
+458 107 _l
+208 615 _l
+208 52 _l
+301 52 _l
+301 0 _l
+55 0 _l
+_cl}_d
+/O{{820 0 56 -13 764 742 _sc
+410 38 _m
+490 38 550 65 591 120 _c
+631 175 652 256 652 364 _c
+652 471 631 552 591 607 _c
+550 662 490 690 410 690 _c
+330 690 269 662 229 607 _c
+188 552 168 471 168 364 _c
+168 256 188 175 229 120 _c
+269 65 330 38 410 38 _c
+410 -13 _m
+360 -13 315 -5 273 11 _c
+231 27 195 50 164 82 _c
+127 118 100 160 82 206 _c
+64 252 56 304 56 364 _c
+56 422 64 475 82 521 _c
+100 567 127 609 164 646 _c
+196 678 232 702 273 718 _c
+314 734 360 742 410 742 _c
+516 742 601 707 666 638 _c
+731 568 764 477 764 364 _c
+764 305 755 252 737 206 _c
+719 159 692 118 656 82 _c
+624 50 587 26 546 10 _c
+505 -5 460 -13 410 -13 _c
+_cl}_e}_d
+/Q{{820 0 56 -159 764 742 _sc
+422 -13 _m
+310 -13 221 21 155 89 _c
+89 157 56 248 56 364 _c
+56 422 64 475 82 521 _c
+100 567 127 609 164 646 _c
+196 678 232 702 273 718 _c
+314 734 360 742 410 742 _c
+516 742 601 707 666 638 _c
+731 568 764 477 764 364 _c
+764 267 739 186 691 120 _c
+642 54 575 13 489 -5 _c
+506 -27 527 -43 553 -53 _c
+578 -63 608 -69 644 -69 _c
+659 -69 _l
+659 -159 _l
+604 -156 557 -142 518 -118 _c
+478 -94 446 -59 422 -13 _c
+410 38 _m
+490 38 550 65 591 120 _c
+631 175 652 256 652 364 _c
+652 471 631 552 591 607 _c
+550 662 490 690 410 690 _c
+330 690 269 662 229 607 _c
+188 552 168 471 168 364 _c
+168 256 188 175 229 120 _c
+269 65 330 38 410 38 _c
+_cl}_e}_d
+/R{{753 0 55 0 777 729 _sc
+479 362 _m
+501 356 521 345 538 330 _c
+554 315 569 294 582 268 _c
+688 52 _l
+777 52 _l
+777 0 _l
+605 0 _l
+491 232 _l
+469 276 449 305 431 319 _c
+413 332 388 339 356 339 _c
+247 339 _l
+247 52 _l
+350 52 _l
+350 0 _l
+55 0 _l
+55 52 _l
+148 52 _l
+148 677 _l
+55 677 _l
+55 729 _l
+425 729 _l
+495 729 550 712 589 678 _c
+627 644 647 596 647 534 _c
+647 484 633 444 605 416 _c
+577 387 535 369 479 362 _c
+247 391 _m
+391 391 _l
+440 391 476 402 500 426 _c
+523 449 535 485 535 534 _c
+}_e{535 582 523 618 500 642 _c
+476 665 440 677 391 677 _c
+247 677 _l
+247 391 _l
+_cl}_e}_d
+/a{{596 0 50 -13 568 533 _sc
+398 163 _m
+398 273 _l
+282 273 _l
+237 273 204 263 182 244 _c
+160 224 150 195 150 156 _c
+150 120 161 91 183 70 _c
+205 48 235 38 273 38 _c
+310 38 340 49 363 72 _c
+386 95 398 125 398 163 _c
+488 324 _m
+488 52 _l
+568 52 _l
+568 0 _l
+398 0 _l
+398 56 _l
+378 32 355 14 329 3 _c
+303 -7 272 -13 238 -13 _c
+180 -13 134 2 100 32 _c
+66 62 50 104 50 156 _c
+50 209 69 250 108 280 _c
+146 310 201 325 272 325 _c
+398 325 _l
+398 361 _l
+398 400 386 430 362 452 _c
+338 474 304 485 261 485 _c
+225 485 197 476 176 460 _c
+154 444 141 420 136 388 _c
+90 388 _l
+}_e{90 493 _l
+121 506 151 516 181 523 _c
+210 529 239 533 267 533 _c
+339 533 393 515 431 479 _c
+469 443 488 392 488 324 _c
+_cl}_e}_d
+/b{{640 0 29 -13 590 760 _sc
+115 52 _m
+115 708 _l
+29 708 _l
+29 760 _l
+205 760 _l
+205 438 _l
+222 470 244 494 272 510 _c
+299 525 333 533 373 533 _c
+437 533 489 507 529 457 _c
+569 407 590 341 590 260 _c
+590 178 569 112 529 62 _c
+489 12 437 -13 373 -13 _c
+333 -13 299 -5 272 9 _c
+244 24 222 48 205 81 _c
+205 0 _l
+29 0 _l
+29 52 _l
+115 52 _l
+205 234 _m
+205 171 217 123 241 91 _c
+265 58 299 42 345 42 _c
+391 42 425 60 449 97 _c
+473 133 485 188 485 260 _c
+485 332 473 386 449 422 _c
+425 458 391 477 345 477 _c
+299 477 265 460 241 427 _c
+217 394 205 347 205 285 _c
+205 234 _l
+}_e{_cl}_e}_d
+/c{{560 0 50 -13 514 533 _sc
+514 156 _m
+501 100 477 58 441 30 _c
+405 1 358 -13 301 -13 _c
+225 -13 165 11 119 61 _c
+73 111 50 177 50 260 _c
+50 342 73 408 119 458 _c
+165 508 225 533 301 533 _c
+333 533 366 529 399 521 _c
+431 513 464 502 497 487 _c
+497 354 _l
+445 354 _l
+438 399 423 432 400 453 _c
+377 474 344 485 302 485 _c
+253 485 216 466 192 428 _c
+167 390 155 334 155 260 _c
+155 184 167 128 192 90 _c
+216 52 253 34 302 34 _c
+340 34 371 44 394 64 _c
+417 84 433 115 442 156 _c
+514 156 _l
+_cl}_e}_d
+/d{{640 0 50 -13 611 760 _sc
+525 52 _m
+611 52 _l
+611 0 _l
+435 0 _l
+435 81 _l
+417 48 395 24 368 9 _c
+340 -5 307 -13 267 -13 _c
+203 -13 150 12 110 62 _c
+70 112 50 178 50 260 _c
+50 341 70 407 110 457 _c
+150 507 203 533 267 533 _c
+307 533 340 525 368 510 _c
+395 494 417 470 435 438 _c
+435 708 _l
+350 708 _l
+350 760 _l
+525 760 _l
+525 52 _l
+435 234 _m
+435 285 _l
+435 347 423 394 399 427 _c
+375 460 340 477 295 477 _c
+249 477 214 458 190 422 _c
+166 386 155 332 155 260 _c
+155 188 166 133 190 97 _c
+214 60 249 42 295 42 _c
+340 42 375 58 399 91 _c
+423 123 435 171 435 234 _c
+}_e{_cl}_e}_d
+/e{{592 0 50 -13 542 533 _sc
+542 250 _m
+155 250 _l
+155 246 _l
+155 176 168 123 194 87 _c
+220 51 259 34 311 34 _c
+350 34 382 44 408 65 _c
+433 85 451 116 461 157 _c
+533 157 _l
+519 100 492 57 454 29 _c
+415 1 364 -13 302 -13 _c
+226 -13 165 11 119 61 _c
+73 111 50 177 50 260 _c
+50 342 72 408 118 458 _c
+163 508 222 533 296 533 _c
+374 533 435 508 477 460 _c
+519 412 540 342 542 250 _c
+436 302 _m
+434 362 421 408 397 439 _c
+373 469 340 485 296 485 _c
+254 485 222 469 198 438 _c
+174 407 160 362 155 302 _c
+436 302 _l
+_cl}_e}_d
+/g{{640 0 50 -221 611 533 _sc
+525 467 _m
+525 11 _l
+525 -63 504 -120 463 -160 _c
+422 -200 364 -221 288 -221 _c
+254 -221 221 -218 190 -212 _c
+158 -206 128 -196 100 -184 _c
+100 -75 _l
+147 -75 _l
+153 -109 166 -133 188 -149 _c
+210 -165 241 -173 282 -173 _c
+334 -173 373 -158 398 -128 _c
+422 -98 435 -51 435 11 _c
+435 81 _l
+417 48 395 24 368 9 _c
+340 -5 307 -13 267 -13 _c
+203 -13 150 12 110 62 _c
+70 112 50 178 50 260 _c
+50 341 70 407 110 457 _c
+150 507 203 533 267 533 _c
+307 533 340 525 368 510 _c
+395 494 417 470 435 438 _c
+435 519 _l
+611 519 _l
+611 467 _l
+525 467 _l
+435 285 _m
+}_e{435 347 423 394 399 427 _c
+375 460 340 477 295 477 _c
+249 477 214 458 190 422 _c
+166 386 155 332 155 260 _c
+155 188 166 133 190 97 _c
+214 60 249 42 295 42 _c
+340 42 375 58 399 91 _c
+423 123 435 171 435 234 _c
+435 285 _l
+_cl}_e}_d
+/h{{644 0 36 0 616 760 _sc
+41 0 _m
+41 52 _l
+122 52 _l
+122 708 _l
+36 708 _l
+36 760 _l
+212 760 _l
+212 427 _l
+228 461 250 488 276 506 _c
+302 524 333 533 369 533 _c
+426 533 468 516 495 484 _c
+522 451 536 400 536 330 _c
+536 52 _l
+616 52 _l
+616 0 _l
+368 0 _l
+368 52 _l
+446 52 _l
+446 302 _l
+446 365 438 408 422 432 _c
+406 455 379 467 340 467 _c
+298 467 266 451 244 421 _c
+222 391 212 347 212 289 _c
+212 52 _l
+290 52 _l
+290 0 _l
+41 0 _l
+_cl}_e}_d
+/i{320 0 36 0 297 736 _sc
+97 680 _m
+97 695 102 708 113 719 _c
+124 730 137 736 153 736 _c
+167 736 180 730 191 719 _c
+202 708 208 695 208 680 _c
+208 664 202 651 192 641 _c
+181 630 168 625 153 625 _c
+137 625 124 630 113 641 _c
+102 651 97 664 97 680 _c
+212 52 _m
+297 52 _l
+297 0 _l
+36 0 _l
+36 52 _l
+122 52 _l
+122 467 _l
+36 467 _l
+36 519 _l
+212 519 _l
+212 52 _l
+_cl}_d
+/k{{606 0 29 0 613 760 _sc
+286 0 _m
+34 0 _l
+34 52 _l
+115 52 _l
+115 708 _l
+29 708 _l
+29 760 _l
+205 760 _l
+205 265 _l
+424 467 _l
+349 467 _l
+349 519 _l
+584 519 _l
+584 467 _l
+495 467 _l
+341 324 _l
+538 52 _l
+613 52 _l
+613 0 _l
+357 0 _l
+357 52 _l
+431 52 _l
+276 265 _l
+205 199 _l
+205 52 _l
+286 52 _l
+286 0 _l
+_cl}_e}_d
+/l{320 0 29 0 290 760 _sc
+205 52 _m
+290 52 _l
+290 0 _l
+29 0 _l
+29 52 _l
+115 52 _l
+115 708 _l
+29 708 _l
+29 760 _l
+205 760 _l
+205 52 _l
+_cl}_d
+/n{{644 0 36 0 616 533 _sc
+41 0 _m
+41 52 _l
+122 52 _l
+122 467 _l
+36 467 _l
+36 519 _l
+212 519 _l
+212 427 _l
+228 461 250 488 276 506 _c
+302 524 333 533 369 533 _c
+426 533 468 516 495 484 _c
+522 451 536 400 536 330 _c
+536 52 _l
+616 52 _l
+616 0 _l
+368 0 _l
+368 52 _l
+446 52 _l
+446 302 _l
+446 365 438 408 422 432 _c
+406 456 379 468 340 468 _c
+298 468 266 452 244 422 _c
+222 391 212 347 212 289 _c
+212 52 _l
+290 52 _l
+290 0 _l
+41 0 _l
+_cl}_e}_d
+/o{602 0 50 -13 552 533 _sc
+301 34 _m
+349 34 385 53 410 91 _c
+434 129 447 185 447 260 _c
+447 334 434 390 410 428 _c
+385 466 349 485 301 485 _c
+253 485 216 466 192 428 _c
+167 390 155 334 155 260 _c
+155 185 167 129 192 91 _c
+216 53 253 34 301 34 _c
+301 -13 _m
+225 -13 165 11 119 61 _c
+73 111 50 177 50 260 _c
+50 342 72 408 118 458 _c
+164 508 225 533 301 533 _c
+377 533 437 508 483 458 _c
+529 408 552 342 552 260 _c
+552 177 529 111 483 61 _c
+437 11 377 -13 301 -13 _c
+_cl}_d
+/r{478 0 36 0 478 533 _sc
+478 520 _m
+478 390 _l
+426 390 _l
+424 416 417 435 405 448 _c
+392 460 373 467 349 467 _c
+305 467 271 451 247 421 _c
+223 390 212 346 212 289 _c
+212 52 _l
+316 52 _l
+316 0 _l
+41 0 _l
+41 52 _l
+122 52 _l
+122 468 _l
+36 468 _l
+36 519 _l
+212 519 _l
+212 427 _l
+229 463 251 489 279 507 _c
+307 524 341 533 381 533 _c
+395 533 411 531 427 529 _c
+443 527 460 524 478 520 _c
+_cl}_d
+/s{{513 0 56 -13 462 533 _sc
+56 29 _m
+56 150 _l
+108 150 _l
+109 111 121 82 144 63 _c
+167 43 201 34 246 34 _c
+286 34 317 41 338 57 _c
+359 72 370 94 370 123 _c
+370 145 362 164 347 178 _c
+331 192 299 207 249 223 _c
+184 245 _l
+139 259 107 277 87 299 _c
+67 320 57 347 57 381 _c
+57 428 74 465 109 492 _c
+144 519 192 533 254 533 _c
+281 533 310 529 340 522 _c
+370 515 402 505 434 491 _c
+434 378 _l
+382 378 _l
+380 411 369 437 347 456 _c
+325 475 295 485 257 485 _c
+219 485 190 478 171 465 _c
+151 451 142 431 142 405 _c
+142 383 149 365 164 352 _c
+178 339 208 326 252 312 _c
+323 290 _l
+372 274 407 255 429 232 _c
+451 209 462 180 462 144 _c
+}_e{462 94 443 56 405 28 _c
+367 0 316 -13 250 -13 _c
+216 -13 184 -9 152 -3 _c
+120 3 88 14 56 29 _c
+_cl}_e}_d
+/t{402 0 29 -13 394 680 _sc
+108 467 _m
+29 467 _l
+29 519 _l
+108 519 _l
+108 680 _l
+198 680 _l
+198 519 _l
+367 519 _l
+367 467 _l
+198 467 _l
+198 137 _l
+198 93 202 64 211 52 _c
+219 40 235 34 258 34 _c
+281 34 298 41 309 55 _c
+319 69 325 91 326 122 _c
+394 122 _l
+391 74 378 40 355 19 _c
+332 -2 297 -13 250 -13 _c
+198 -13 161 -1 140 21 _c
+118 43 108 82 108 137 _c
+108 467 _l
+_cl}_d
+/u{{644 0 27 -13 607 519 _sc
+354 519 _m
+522 519 _l
+522 52 _l
+607 52 _l
+607 0 _l
+432 0 _l
+432 92 _l
+415 57 393 31 367 13 _c
+341 -4 310 -13 276 -13 _c
+218 -13 175 3 148 35 _c
+121 67 108 119 108 189 _c
+108 467 _l
+27 467 _l
+27 519 _l
+198 519 _l
+198 217 _l
+198 153 205 110 221 87 _c
+237 63 264 52 304 52 _c
+346 52 377 67 399 98 _c
+421 128 432 173 432 231 _c
+432 467 _l
+354 467 _l
+354 519 _l
+_cl}_e}_d
+/v{565 0 -2 0 562 519 _sc
+247 0 _m
+56 467 _l
+-2 467 _l
+-2 519 _l
+236 519 _l
+236 467 _l
+153 467 _l
+299 110 _l
+445 467 _l
+367 467 _l
+367 519 _l
+562 519 _l
+562 467 _l
+504 467 _l
+313 0 _l
+247 0 _l
+_cl}_d
+/w{856 0 16 0 843 519 _sc
+480 519 _m
+613 114 _l
+730 467 _l
+655 467 _l
+655 519 _l
+843 519 _l
+843 467 _l
+785 467 _l
+631 0 _l
+556 0 _l
+428 388 _l
+300 0 _l
+228 0 _l
+74 467 _l
+16 467 _l
+16 519 _l
+251 519 _l
+251 467 _l
+167 467 _l
+283 114 _l
+417 519 _l
+480 519 _l
+_cl}_d
+end readonly def
+
+/BuildGlyph
+ {exch begin
+ CharStrings exch
+ 2 copy known not{pop /.notdef}if
+ true 3 1 roll get exec
+ end}_d
+
+/BuildChar {
+ 1 index /Encoding get exch get
+ 1 index /BuildGlyph get exec
+}_d
+
+FontName currentdict end definefont pop
+%% Font Page 00
+/BitstreamVeraSerif-Roman-ENC-00 [
+/.notdef/space/colon/C/l/i/e/n/t/A/g/M/a/r/parenleft/b/k/o/d/parenright/R/s/u
+/c/h/I/D/B/v/O/w/Q/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef] def
+/BitstreamVeraSerif-Roman-Uni-00 BitstreamVeraSerif-Roman-ENC-00 /BitstreamVeraSerif-Roman MFEmb
+%%BeginFont: BitstreamVeraSerif-Roman
+%!PS-AdobeFont-1.0 Composite Font
+%%FontName: BitstreamVeraSerif-Roman-Uni
+%%Creator: Composite font created by Qt
+25 dict begin
+/FontName /BitstreamVeraSerif-Roman-Uni def
+/PaintType 0 def
+/FontMatrix[1 0 0 1 0 0]def
+/FontType 0 def
+/FMapType 2 def
+/Encoding [
+0]def
+/FDepVector [
+/BitstreamVeraSerif-Roman-Uni-00 findfont
+]def
+FontName currentdict end definefont pop
+%%EndFont
+%%EndFont
+/F1 8.33333/BitstreamVeraSerif-Roman-Uni DF
+%%EndSetup
+%%Page: 1 1
+%%BeginPageSetup
+QI
+%%EndPageSetup
+[1 0 0 1 -42 -48]ST
+0 0 B 0 0 PE
+WB
+W BC
+42 48 597 168 R
+2 0 255 0 0 0 0 PE
+NB
+67 216 69 VL
+252 216 69 VL
+432 202 69 VL
+610 189 69 VL
+0 0 B 0 0 PE
+WB
+243 100 17 32 R
+255 0 0 P1
+NB
+243 100 17 32 R
+243 104 67 HL
+1 255 0 0 BR
+NP
+242 104 MT
+238 101 LT
+238 107 LT
+CP BF QS
+71 126 67 129 DL
+71 132 67 129 DL
+3 0 255 0 0 0 0 PE
+243 129 67 HL
+0 0 B 0 0 PE
+WB
+423 101 17 32 R
+255 0 0 P1
+NB
+423 101 17 32 R
+423 105 260 HL
+1 255 0 0 BR
+NP
+422 105 MT
+418 102 LT
+418 108 LT
+CP BF QS
+264 127 260 130 DL
+264 133 260 130 DL
+3 0 255 0 0 0 0 PE
+423 130 260 HL
+255 0 0 P1
+NB
+604 110 608 113 DL
+604 116 608 113 DL
+608 113 440 HL
+255 0 0 P1
+437 162 433 165 DL
+437 168 433 165 DL
+608 165 433 HL
+255 0 0 P1
+257 175 253 178 DL
+257 181 253 178 DL
+430 178 253 HL
+255 0 0 P1
+72 189 68 192 DL
+72 195 68 192 DL
+250 192 68 HL
+255 0 0 P1
+1 255 255 192 BR
+42 48 50 21 R
+CLSTART
+5 5 40 11 ACR
+CLEND
+B P1
+F1 F
+61 Y<000100020001000300040005000600070008>[3 0 3 0 3 0 6 0 3 0 3 0 5 0 5 0 0 0]34 50 63 1 Tl XYT
+CLO
+[1 0 0 1 -42 -48]ST
+255 0 0 P1
+1 255 255 192 BR
+186 48 133 21 R
+CLO
+[1 0 0 1 -42 -48]ST
+CLSTART
+149 5 123 11 ACR
+CLEND
+B P1
+1 255 255 192 BR
+F1 F
+<0001000200010009000A000600070008000B000C0007000C000A0006000D0001000E00040005000F000C001000110007000C001200050013>[3 0 3 0 3 0 6 0 5 0 5 0 5 0 3 0 8 0 5 0 5 0 5 0 5 0 5 0 4 0 3 0 3 0 3 0 3 0 5 0 5 0 5 0 5 0 5 0 5 0 5 0 3 0 0 0]123 191 63 1 Tl XYT
+CLO
+[1 0 0 1 -42 -48]ST
+255 0 0 P1
+1 255 255 192 BR
+407 48 50 21 R
+CLO
+[1 0 0 1 -42 -48]ST
+CLSTART
+370 5 40 11 ACR
+CLEND
+B P1
+1 255 255 192 BR
+F1 F
+<0001000200010003001100070008000D00110004>[3 0 3 0 3 0 6 0 5 0 5 0 3 0 4 0 5 0 0 0]40 412 63 1 Tl XYT
+CLO
+[1 0 0 1 -42 -48]ST
+255 0 0 P1
+1 255 255 192 BR
+582 48 57 21 R
+CLO
+[1 0 0 1 -42 -48]ST
+CLSTART
+545 5 47 11 ACR
+CLEND
+B P1
+1 255 255 192 BR
+F1 F
+<00010002000100140006001500110016000D00170006>[3 0 3 0 3 0 6 0 5 0 4 0 5 0 5 0 4 0 4 0 0 0]47 587 63 1 Tl XYT
+CLO
+[1 0 0 1 -42 -48]ST
+CLSTART
+35 37 147 15 ACR
+CLEND
+50 d2 P1
+NB
+F1 F
+95 Y<000B0006000800180011001200010017000C00040004000200010017000D0006000C000800060009000A0006000700080019000700150008000C000700170006>[8 0 5 0 3 0 5 0 5 0 5 0 3 0 4 0 5 0 3 0 3 0 3 0 3 0 4 0 4 0 5 0 5 0 3 0 5 0 6 0 5 0 5 0 5 0 3 0 3 0 5 0 4 0 3 0 5 0 5 0 4 0 0 0]139 81 XYT
+CLO
+[1 0 0 1 -42 -48]ST
+CLSTART
+215 38 137 15 ACR
+CLEND
+50 d2 P1
+NB
+F1 F
+96 Y<001A001B0016001500010017000C00040004000200010017000D0006000C000800060009000A0006000700080019000700150008000C000700170006>[6 0 6 0 5 0 4 0 3 0 4 0 5 0 3 0 3 0 3 0 3 0 4 0 4 0 5 0 5 0 3 0 5 0 6 0 5 0 5 0 5 0 3 0 3 0 5 0 4 0 3 0 5 0 5 0 4 0 0 0]129 261 XYT
+CLO
+[1 0 0 1 -42 -48]ST
+CLSTART
+399 46 118 15 ACR
+CLEND
+50 d2 P1
+NB
+F1 F
+104 Y<0009001700080005001100070002000100150008000C000D0008001500010008001800060001000D0006001500110016000D00170006>[6 0 4 0 3 0 3 0 5 0 5 0 3 0 3 0 4 0 3 0 5 0 4 0 3 0 4 0 3 0 3 0 5 0 5 0 3 0 4 0 5 0 4 0 5 0 5 0 4 0 4 0 0 0]110 445 XYT
+CLO
+[1 0 0 1 -42 -48]ST
+CLSTART
+399 98 155 15 ACR
+CLEND
+50 d2 P1
+NB
+F1 F
+156 Y<001A001B00160015000100150005000A0007000C00040002000100150006000D001C000500170006001D001E00070006000D00030018000C0007000A00060012>[6 0 6 0 5 0 4 0 3 0 4 0 3 0 5 0 5 0 5 0 3 0 3 0 3 0 4 0 5 0 4 0 4 0 3 0 4 0 5 0 6 0 7 0 5 0 5 0 4 0 6 0 5 0 5 0 5 0 5 0 5 0 0 0]147 445 XYT
+CLO
+[1 0 0 1 -42 -48]ST
+CLSTART
+216 111 155 15 ACR
+CLEND
+50 d2 P1
+NB
+F1 F
+169 Y<001A001B00160015000100150005000A0007000C000400020001000C000A0006000700080019000700150008000C00070017000600090012001200060012>[6 0 6 0 5 0 4 0 3 0 4 0 3 0 5 0 5 0 5 0 3 0 3 0 3 0 5 0 5 0 5 0 5 0 3 0 3 0 5 0 4 0 3 0 5 0 5 0 4 0 5 0 6 0 5 0 5 0 5 0 0 0]138 266 XYT
+CLO
+[1 0 0 1 -42 -48]ST
+CLSTART
+39 125 134 15 ACR
+CLEND
+50 d2 P1
+NB
+F1 F
+183 Y<001F0008000100150005000A0007000C000400020001000C000A0006000700080019000700150008000C00070017000600090012001200060012>[6 0 3 0 3 0 4 0 3 0 5 0 5 0 5 0 3 0 3 0 3 0 5 0 5 0 5 0 5 0 3 0 3 0 5 0 4 0 3 0 5 0 5 0 4 0 5 0 6 0 5 0 5 0 5 0 0 0]126 85 XYT
+
+QP
+%%Trailer
+%%Pages: 1
+%%DocumentFonts: BitstreamVeraSerif-Roman
+%%EOF
diff --git a/doc/pics/akonadi_agent_handling.png b/doc/pics/akonadi_agent_handling.png
new file mode 100644 (file)
index 0000000..ef14ec5
Binary files /dev/null and b/doc/pics/akonadi_agent_handling.png differ
diff --git a/doc/pics/akonadi_agent_handling_small.png b/doc/pics/akonadi_agent_handling_small.png
new file mode 100644 (file)
index 0000000..11678bd
Binary files /dev/null and b/doc/pics/akonadi_agent_handling_small.png differ
diff --git a/doc/pics/akonadi_client_search.eps b/doc/pics/akonadi_client_search.eps
new file mode 100644 (file)
index 0000000..ed6d8ec
--- /dev/null
@@ -0,0 +1,1282 @@
+%!PS-Adobe-1.0 EPSF-3.0
+%%BoundingBox: 35 611 641 819
+%%Creator: Qt 3.3.6
+%%CreationDate: Do Sep 28 16:06:56 2006
+%%Orientation: Portrait
+%%Pages: 1
+%%DocumentFonts: BitstreamVeraSerif-Roman
+
+%%EndComments
+%%BeginProlog
+% Prolog copyright 1994-2005 Trolltech. You may copy this prolog in any way
+% that is directly related to this document. For other use of this prolog,
+% see your licensing agreement for Qt.
+/d/def load def/D{bind d}bind d/d2{dup dup}D/B{0 d2}D/W{255 d2}D/ED{exch d}D
+/D0{0 ED}D/LT{lineto}D/MT{moveto}D/S{stroke}D/F{setfont}D/SW{setlinewidth}D
+/CP{closepath}D/RL{rlineto}D/NP{newpath}D/CM{currentmatrix}D/SM{setmatrix}D
+/TR{translate}D/SD{setdash}D/SC{aload pop setrgbcolor}D/CR{currentfile read
+pop}D/i{index}D/bs{bitshift}D/scs{setcolorspace}D/DB{dict dup begin}D/DE{end
+d}D/ie{ifelse}D/sp{astore pop}D/BSt 0 d/LWi 1 d/PSt 1 d/Cx 0 d/Cy 0 d/WFi
+false d/OMo false d/BCol[1 1 1]d/PCol[0 0 0]d/BkCol[1 1 1]d/BDArr[0.94 0.88
+0.63 0.50 0.37 0.12 0.06]d/defM matrix d/nS 0 d/GPS{PSt 1 ge PSt 5 le and{{
+LArr PSt 1 sub 2 mul get}{LArr PSt 2 mul 1 sub get}ie}{[]}ie}D/QS{PSt 0 ne{
+gsave LWi SW true GPS 0 SD S OMo PSt 1 ne and{BkCol SC false GPS dup 0 get
+SD S}if grestore}if}D/r28{{CR dup 32 gt{exit}if pop}loop 3{CR}repeat 0 4{7
+bs exch dup 128 gt{84 sub}if 42 sub 127 and add}repeat}D/rA 0 d/rL 0 d/rB{rL
+0 eq{/rA r28 d/rL 28 d}if dup rL gt{rA exch rL sub rL exch/rA 0 d/rL 0 d rB
+exch bs add}{dup rA 16#fffffff 3 -1 roll bs not and exch dup rL exch sub/rL
+ED neg rA exch bs/rA ED}ie}D/uc{/rL 0 d 0{dup 2 i length ge{exit}if 1 rB 1
+eq{3 rB dup 3 ge{1 add dup rB 1 i 5 ge{1 i 6 ge{1 i 7 ge{1 i 8 ge{128 add}if
+64 add}if 32 add}if 16 add}if 3 add exch pop}if 3 add exch 10 rB 1 add{dup 3
+i lt{dup}{2 i}ie 4 i 3 i 3 i sub 2 i getinterval 5 i 4 i 3 -1 roll
+putinterval dup 4 -1 roll add 3 1 roll 4 -1 roll exch sub dup 0 eq{exit}if 3
+1 roll}loop pop pop}{3 rB 1 add{2 copy 8 rB put 1 add}repeat}ie}loop pop}D
+/sl D0/QCIgray D0/QCIcolor D0/QCIindex D0/QCI{/colorimage where{pop false 3
+colorimage}{exec/QCIcolor ED/QCIgray QCIcolor length 3 idiv string d 0 1
+QCIcolor length 3 idiv 1 sub{/QCIindex ED/x QCIindex 3 mul d QCIgray
+QCIindex QCIcolor x get 0.30 mul QCIcolor x 1 add get 0.59 mul QCIcolor x 2
+add get 0.11 mul add add cvi put}for QCIgray image}ie}D/di{gsave TR 1 i 1 eq
+{false eq{pop true 3 1 roll 4 i 4 i false 4 i 4 i imagemask BkCol SC
+imagemask}{pop false 3 1 roll imagemask}ie}{dup false ne{/languagelevel
+where{pop languagelevel 3 ge}{false}ie}{false}ie{/ma ED 8 eq{/dc[0 1]d
+/DeviceGray}{/dc[0 1 0 1 0 1]d/DeviceRGB}ie scs/im ED/mt ED/h ED/w ED/id 7
+DB/ImageType 1 d/Width w d/Height h d/ImageMatrix mt d/DataSource im d
+/BitsPerComponent 8 d/Decode dc d DE/md 7 DB/ImageType 1 d/Width w d/Height
+h d/ImageMatrix mt d/DataSource ma d/BitsPerComponent 1 d/Decode[0 1]d DE 4
+DB/ImageType 3 d/DataDict id d/MaskDict md d/InterleaveType 3 d end image}{
+pop 8 4 1 roll 8 eq{image}{QCI}ie}ie}ie grestore}d/BF{gsave BSt 1 eq{BCol SC
+WFi{fill}{eofill}ie}if BSt 2 ge BSt 8 le and{BDArr BSt 2 sub get/sc ED BCol{
+1. exch sub sc mul 1. exch sub}forall 3 array astore SC WFi{fill}{eofill}ie}
+if BSt 9 ge BSt 14 le and{WFi{clip}{eoclip}ie defM SM pathbbox 3 i 3 i TR 4
+2 roll 3 2 roll exch sub/h ED sub/w ED OMo{NP 0 0 MT 0 h RL w 0 RL 0 h neg
+RL CP BkCol SC fill}if BCol SC 0.3 SW NP BSt 9 eq BSt 11 eq or{0 4 h{dup 0
+exch MT w exch LT}for}if BSt 10 eq BSt 11 eq or{0 4 w{dup 0 MT h LT}for}if
+BSt 12 eq BSt 14 eq or{w h gt{0 6 w h add{dup 0 MT h sub h LT}for}{0 6 w h
+add{dup 0 exch MT w sub w exch LT}for}ie}if BSt 13 eq BSt 14 eq or{w h gt{0
+6 w h add{dup h MT h sub 0 LT}for}{0 6 w h add{dup w exch MT w sub 0 exch LT
+}for}ie}if S}if BSt 24 eq{}if grestore}D/mat matrix d/ang1 D0/ang2 D0/w D0/h
+D0/x D0/y D0/ARC{/ang2 ED/ang1 ED/h ED/w ED/y ED/x ED mat CM pop x w 2 div
+add y h 2 div add TR 1 h w div neg scale ang2 0 ge{0 0 w 2 div ang1 ang1
+ang2 add arc}{0 0 w 2 div ang1 ang1 ang2 add arcn}ie mat SM}D/C D0/P{NP MT
+0.5 0.5 rmoveto 0 -1 RL -1 0 RL 0 1 RL CP fill}D/M{/Cy ED/Cx ED}D/L{NP Cx Cy
+MT/Cy ED/Cx ED Cx Cy LT QS}D/DL{NP MT LT QS}D/HL{1 i DL}D/VL{2 i exch DL}D/R
+{/h ED/w ED/y ED/x ED NP x y MT 0 h RL w 0 RL 0 h neg RL CP BF QS}D/ACR{/h
+ED/w ED/y ED/x ED x y MT 0 h RL w 0 RL 0 h neg RL CP}D/xr D0/yr D0/rx D0/ry
+D0/rx2 D0/ry2 D0/RR{/yr ED/xr ED/h ED/w ED/y ED/x ED xr 0 le yr 0 le or{x y
+w h R}{xr 100 ge yr 100 ge or{x y w h E}{/rx xr w mul 200 div d/ry yr h mul
+200 div d/rx2 rx 2 mul d/ry2 ry 2 mul d NP x rx add y MT x y rx2 ry2 180 -90
+x y h add ry2 sub rx2 ry2 270 -90 x w add rx2 sub y h add ry2 sub rx2 ry2 0
+-90 x w add rx2 sub y rx2 ry2 90 -90 ARC ARC ARC ARC CP BF QS}ie}ie}D/E{/h
+ED/w ED/y ED/x ED mat CM pop x w 2 div add y h 2 div add TR 1 h w div scale
+NP 0 0 w 2 div 0 360 arc mat SM BF QS}D/A{16 div exch 16 div exch NP ARC QS}
+D/PIE{/ang2 ED/ang1 ED/h ED/w ED/y ED/x ED NP x w 2 div add y h 2 div add MT
+x y w h ang1 16 div ang2 16 div ARC CP BF QS}D/CH{16 div exch 16 div exch NP
+ARC CP BF QS}D/BZ{curveto QS}D/CRGB{255 div 3 1 roll 255 div 3 1 roll 255
+div 3 1 roll}D/BC{CRGB BkCol sp}D/BR{CRGB BCol sp/BSt ED}D/WB{1 W BR}D/NB{0
+B BR}D/PE{setlinejoin setlinecap CRGB PCol sp/LWi ED/PSt ED LWi 0 eq{0.25
+/LWi ED}if PCol SC}D/P1{1 0 5 2 roll 0 0 PE}D/ST{defM SM concat}D/MF{true
+exch true exch{exch pop exch pop dup 0 get dup findfont dup/FontName get 3
+-1 roll eq{exit}if}forall exch dup 1 get/fxscale ED 2 get/fslant ED exch
+/fencoding ED[fxscale 0 fslant 1 0 0]makefont fencoding false eq{}{dup
+maxlength dict begin{1 i/FID ne{def}{pop pop}ifelse}forall/Encoding
+fencoding d currentdict end}ie definefont pop}D/MFEmb{findfont dup length
+dict begin{1 i/FID ne{d}{pop pop}ifelse}forall/Encoding ED currentdict end
+definefont pop}D/DF{findfont/fs 3 -1 roll d[fs 0 0 fs -1 mul 0 0]makefont d}
+D/ty 0 d/Y{/ty ED}D/Tl{gsave SW NP 1 i exch MT 1 i 0 RL S grestore}D/XYT{ty
+MT/xyshow where{pop pop xyshow}{exch pop 1 i dup length 2 div exch
+stringwidth pop 3 -1 roll exch sub exch div exch 0 exch ashow}ie}D/AT{ty MT
+1 i dup length 2 div exch stringwidth pop 3 -1 roll exch sub exch div exch 0
+exch ashow}D/QI{/C save d pageinit/Cx 0 d/Cy 0 d/OMo false d}D/QP{C restore
+showpage}D/SPD{/setpagedevice where{1 DB 3 1 roll d end setpagedevice}{pop
+pop}ie}D/SV{BSt LWi PSt Cx Cy WFi OMo BCol PCol BkCol/nS nS 1 add d gsave}D
+/RS{nS 0 gt{grestore/BkCol ED/PCol ED/BCol ED/OMo ED/WFi ED/Cy ED/Cx ED/PSt
+ED/LWi ED/BSt ED/nS nS 1 sub d}if}D/CLSTART{/clipTmp matrix CM d defM SM NP}
+D/CLEND{clip NP clipTmp SM}D/CLO{grestore gsave defM SM}D
+
+/LArr[ [] [] [ 10.417 3.125 ] [ 3.125 10.417 ] [ 3.125 3.125 ] [ 3.125 3.125 ] [ 5.208 3.125 3.125 3.125 ] [ 3.125 5.208 3.125 3.125 ] [ 5.208 3.125 3.125 3.125 3.125 ] [ 3.125 5.208 3.125 3.125 3.125 3.125 ] ] d
+/pageinit {
+35.52 24 translate
+% 185*280mm (portrait)
+0 793.92 translate 0.96 -0.96 scale/defM matrix CM d } d
+%%EndProlog
+%%BeginSetup
+% Fonts and encodings used
+/BitstreamVeraSerif-RomanList [
+[ /BitstreamVeraSerif-Roman 1.0 0.0 ]
+  [ /BitstreamVeraSerif 1.0 0.0 ]
+  [ /Helvetica 0.988 0.000 ]
+] d
+%%BeginFont: Bitstream Vera Serif
+%!PS-Adobe-3.0 Resource-Font
+%%Copyright: Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved.
+%%Creator: Converted from TrueType by Qt
+25 dict begin
+/_d{bind def}bind def
+/_m{moveto}_d
+/_l{lineto}_d
+/_cl{closepath eofill}_d
+/_c{curveto}_d
+/_sc{7 -1 roll{setcachedevice}{pop pop pop pop pop pop}ifelse}_d
+/_e{exec}_d
+/FontName /BitstreamVeraSerif-Roman def
+/PaintType 0 def
+/FontMatrix[.001 0 0 .001 0 0]def
+/FontBBox[-182 -235 1287 928]def
+/FontType 3 def
+/Encoding StandardEncoding def
+/FontInfo 10 dict dup begin
+/FamilyName (Bitstream Vera Serif) def
+/FullName (Bitstream Vera Serif) def
+/Notice (Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a trademark of Bitstream, Inc.) def
+/Weight (Roman) def
+/Version (Release 1.10) def
+/ItalicAngle 0.0 def
+/isFixedPitch false def
+/UnderlinePosition -213 def
+/UnderlineThickness 133 def
+end readonly def
+/CharStrings 33 dict dup begin
+/.notdef{600 0 50 -176 550 705 _sc
+50 -176 _m
+50 705 _l
+550 705 _l
+550 -176 _l
+50 -176 _l
+106 -120 _m
+494 -120 _l
+494 649 _l
+106 649 _l
+106 -120 _l
+_cl}_d
+/space{318 0 0 0 0 0 _sc
+}_d
+/colon{337 0 104 -13 234 434 _sc
+104 51 _m
+104 69 110 84 123 97 _c
+135 109 151 116 169 116 _c
+187 116 202 109 215 97 _c
+227 84 234 69 234 51 _c
+234 32 227 17 215 5 _c
+203 -7 187 -13 169 -13 _c
+150 -13 134 -7 122 5 _c
+110 17 104 32 104 51 _c
+104 369 _m
+104 387 110 402 123 415 _c
+135 427 151 434 169 434 _c
+187 434 203 427 215 415 _c
+227 403 234 387 234 369 _c
+234 350 227 334 215 322 _c
+203 310 187 304 169 304 _c
+151 304 135 310 123 323 _c
+110 335 104 351 104 369 _c
+_cl}_d
+/A{722 0 -5 0 732 729 _sc
+200 264 _m
+468 264 _l
+334 611 _l
+200 264 _l
+-5 0 _m
+-5 52 _l
+58 52 _l
+318 729 _l
+400 729 _l
+660 52 _l
+732 52 _l
+732 0 _l
+467 0 _l
+467 52 _l
+548 52 _l
+487 212 _l
+180 212 _l
+119 52 _l
+199 52 _l
+199 0 _l
+-5 0 _l
+_cl}_d
+/B{{735 0 55 0 674 729 _sc
+247 52 _m
+393 52 _l
+451 52 494 64 521 90 _c
+548 115 562 155 562 211 _c
+562 265 548 305 521 331 _c
+494 356 451 369 393 369 _c
+247 369 _l
+247 52 _l
+55 0 _m
+55 52 _l
+148 52 _l
+148 677 _l
+55 677 _l
+55 729 _l
+415 729 _l
+488 729 543 714 581 684 _c
+618 654 637 609 637 549 _c
+637 505 624 471 598 445 _c
+572 419 535 404 485 398 _c
+547 390 594 370 626 338 _c
+658 306 674 264 674 211 _c
+674 139 651 85 605 51 _c
+559 17 488 0 392 0 _c
+55 0 _l
+247 421 _m
+371 421 _l
+424 421 463 431 488 451 _c
+512 471 525 504 525 549 _c
+}_e{525 593 512 626 488 646 _c
+463 666 424 677 371 677 _c
+247 677 _l
+247 421 _l
+_cl}_e}_d
+/C{{765 0 56 -13 705 742 _sc
+705 193 _m
+683 125 647 73 597 39 _c
+546 4 482 -13 405 -13 _c
+357 -13 312 -5 272 11 _c
+231 27 195 50 164 82 _c
+127 118 100 160 82 206 _c
+64 252 56 305 56 364 _c
+56 477 88 568 154 638 _c
+219 707 305 742 413 742 _c
+453 742 495 736 540 726 _c
+584 716 633 700 685 679 _c
+685 511 _l
+630 511 _l
+618 572 593 617 557 646 _c
+521 675 470 690 405 690 _c
+327 690 268 662 228 607 _c
+188 551 168 470 168 364 _c
+168 257 188 176 228 121 _c
+268 65 327 38 405 38 _c
+459 38 503 51 539 77 _c
+574 103 599 141 615 193 _c
+705 193 _l
+_cl}_e}_d
+/D{802 0 55 0 744 729 _sc
+247 52 _m
+338 52 _l
+432 52 505 79 556 133 _c
+606 187 632 264 632 365 _c
+632 466 606 543 556 597 _c
+505 650 432 677 338 677 _c
+247 677 _l
+247 52 _l
+55 0 _m
+55 52 _l
+148 52 _l
+148 677 _l
+55 677 _l
+55 729 _l
+345 729 _l
+471 729 569 697 639 633 _c
+709 569 744 479 744 365 _c
+744 250 708 160 638 96 _c
+568 32 470 0 345 0 _c
+55 0 _l
+_cl}_d
+/E{730 0 55 0 650 729 _sc
+55 0 _m
+55 52 _l
+148 52 _l
+148 677 _l
+55 677 _l
+55 729 _l
+642 729 _l
+642 567 _l
+582 567 _l
+582 669 _l
+247 669 _l
+247 425 _l
+486 425 _l
+486 516 _l
+546 516 _l
+546 274 _l
+486 274 _l
+486 365 _l
+247 365 _l
+247 60 _l
+590 60 _l
+590 162 _l
+650 162 _l
+650 0 _l
+55 0 _l
+_cl}_d
+/I{395 0 55 0 340 729 _sc
+247 52 _m
+340 52 _l
+340 0 _l
+55 0 _l
+55 52 _l
+148 52 _l
+148 677 _l
+55 677 _l
+55 729 _l
+340 729 _l
+340 677 _l
+247 677 _l
+247 52 _l
+_cl}_d
+/M{1024 0 50 0 973 729 _sc
+55 0 _m
+55 52 _l
+148 52 _l
+148 677 _l
+50 677 _l
+50 729 _l
+262 729 _l
+518 210 _l
+774 729 _l
+973 729 _l
+973 677 _l
+876 677 _l
+876 52 _l
+969 52 _l
+969 0 _l
+684 0 _l
+684 52 _l
+777 52 _l
+777 615 _l
+527 107 _l
+458 107 _l
+208 615 _l
+208 52 _l
+301 52 _l
+301 0 _l
+55 0 _l
+_cl}_d
+/P{{673 0 55 0 637 729 _sc
+247 371 _m
+376 371 _l
+424 371 461 384 487 410 _c
+512 436 525 474 525 524 _c
+525 574 512 612 487 638 _c
+461 664 424 677 376 677 _c
+247 677 _l
+247 371 _l
+55 0 _m
+55 52 _l
+148 52 _l
+148 677 _l
+55 677 _l
+55 729 _l
+400 729 _l
+472 729 530 710 573 673 _c
+615 636 637 586 637 524 _c
+637 461 615 411 573 374 _c
+530 337 472 319 400 319 _c
+247 319 _l
+247 52 _l
+360 52 _l
+360 0 _l
+55 0 _l
+_cl}_e}_d
+/R{{753 0 55 0 777 729 _sc
+479 362 _m
+501 356 521 345 538 330 _c
+554 315 569 294 582 268 _c
+688 52 _l
+777 52 _l
+777 0 _l
+605 0 _l
+491 232 _l
+469 276 449 305 431 319 _c
+413 332 388 339 356 339 _c
+247 339 _l
+247 52 _l
+350 52 _l
+350 0 _l
+55 0 _l
+55 52 _l
+148 52 _l
+148 677 _l
+55 677 _l
+55 729 _l
+425 729 _l
+495 729 550 712 589 678 _c
+627 644 647 596 647 534 _c
+647 484 633 444 605 416 _c
+577 387 535 369 479 362 _c
+247 391 _m
+391 391 _l
+440 391 476 402 500 426 _c
+523 449 535 485 535 534 _c
+}_e{535 582 523 618 500 642 _c
+476 665 440 677 391 677 _c
+247 677 _l
+247 391 _l
+_cl}_e}_d
+/S{{685 0 84 -13 612 742 _sc
+93 35 _m
+93 201 _l
+149 201 _l
+150 145 166 104 197 78 _c
+227 51 274 38 336 38 _c
+394 38 438 49 468 72 _c
+498 95 514 129 514 173 _c
+514 208 504 235 486 254 _c
+468 272 429 291 370 309 _c
+274 338 _l
+204 359 154 385 126 417 _c
+98 448 84 491 84 547 _c
+84 609 106 657 150 691 _c
+194 725 255 742 335 742 _c
+369 742 406 738 446 731 _c
+486 723 529 713 575 699 _c
+575 544 _l
+520 544 _l
+514 595 497 632 468 655 _c
+439 678 395 690 337 690 _c
+285 690 246 679 219 658 _c
+192 637 179 607 179 567 _c
+179 532 189 505 209 485 _c
+229 465 272 445 338 426 _c
+428 399 _l
+494 379 541 353 569 323 _c
+597 292 612 251 612 199 _c
+}_e{612 128 589 75 544 40 _c
+498 4 431 -13 342 -13 _c
+302 -13 261 -9 219 -1 _c
+177 6 135 18 93 35 _c
+_cl}_e}_d
+/a{{596 0 50 -13 568 533 _sc
+398 163 _m
+398 273 _l
+282 273 _l
+237 273 204 263 182 244 _c
+160 224 150 195 150 156 _c
+150 120 161 91 183 70 _c
+205 48 235 38 273 38 _c
+310 38 340 49 363 72 _c
+386 95 398 125 398 163 _c
+488 324 _m
+488 52 _l
+568 52 _l
+568 0 _l
+398 0 _l
+398 56 _l
+378 32 355 14 329 3 _c
+303 -7 272 -13 238 -13 _c
+180 -13 134 2 100 32 _c
+66 62 50 104 50 156 _c
+50 209 69 250 108 280 _c
+146 310 201 325 272 325 _c
+398 325 _l
+398 361 _l
+398 400 386 430 362 452 _c
+338 474 304 485 261 485 _c
+225 485 197 476 176 460 _c
+154 444 141 420 136 388 _c
+90 388 _l
+}_e{90 493 _l
+121 506 151 516 181 523 _c
+210 529 239 533 267 533 _c
+339 533 393 515 431 479 _c
+469 443 488 392 488 324 _c
+_cl}_e}_d
+/c{{560 0 50 -13 514 533 _sc
+514 156 _m
+501 100 477 58 441 30 _c
+405 1 358 -13 301 -13 _c
+225 -13 165 11 119 61 _c
+73 111 50 177 50 260 _c
+50 342 73 408 119 458 _c
+165 508 225 533 301 533 _c
+333 533 366 529 399 521 _c
+431 513 464 502 497 487 _c
+497 354 _l
+445 354 _l
+438 399 423 432 400 453 _c
+377 474 344 485 302 485 _c
+253 485 216 466 192 428 _c
+167 390 155 334 155 260 _c
+155 184 167 128 192 90 _c
+216 52 253 34 302 34 _c
+340 34 371 44 394 64 _c
+417 84 433 115 442 156 _c
+514 156 _l
+_cl}_e}_d
+/d{{640 0 50 -13 611 760 _sc
+525 52 _m
+611 52 _l
+611 0 _l
+435 0 _l
+435 81 _l
+417 48 395 24 368 9 _c
+340 -5 307 -13 267 -13 _c
+203 -13 150 12 110 62 _c
+70 112 50 178 50 260 _c
+50 341 70 407 110 457 _c
+150 507 203 533 267 533 _c
+307 533 340 525 368 510 _c
+395 494 417 470 435 438 _c
+435 708 _l
+350 708 _l
+350 760 _l
+525 760 _l
+525 52 _l
+435 234 _m
+435 285 _l
+435 347 423 394 399 427 _c
+375 460 340 477 295 477 _c
+249 477 214 458 190 422 _c
+166 386 155 332 155 260 _c
+155 188 166 133 190 97 _c
+214 60 249 42 295 42 _c
+340 42 375 58 399 91 _c
+423 123 435 171 435 234 _c
+}_e{_cl}_e}_d
+/e{{592 0 50 -13 542 533 _sc
+542 250 _m
+155 250 _l
+155 246 _l
+155 176 168 123 194 87 _c
+220 51 259 34 311 34 _c
+350 34 382 44 408 65 _c
+433 85 451 116 461 157 _c
+533 157 _l
+519 100 492 57 454 29 _c
+415 1 364 -13 302 -13 _c
+226 -13 165 11 119 61 _c
+73 111 50 177 50 260 _c
+50 342 72 408 118 458 _c
+163 508 222 533 296 533 _c
+374 533 435 508 477 460 _c
+519 412 540 342 542 250 _c
+436 302 _m
+434 362 421 408 397 439 _c
+373 469 340 485 296 485 _c
+254 485 222 469 198 438 _c
+174 407 160 362 155 302 _c
+436 302 _l
+_cl}_e}_d
+/f{{370 0 36 0 430 760 _sc
+430 637 _m
+383 637 _l
+382 661 375 680 362 693 _c
+348 705 329 712 303 712 _c
+269 712 246 702 232 684 _c
+218 666 212 633 212 586 _c
+212 519 _l
+357 519 _l
+357 467 _l
+212 467 _l
+212 52 _l
+327 52 _l
+327 0 _l
+36 0 _l
+36 52 _l
+122 52 _l
+122 467 _l
+36 467 _l
+36 519 _l
+122 519 _l
+122 584 _l
+122 642 137 685 167 715 _c
+197 745 241 760 300 760 _c
+322 760 343 758 365 754 _c
+387 750 408 744 430 736 _c
+430 637 _l
+_cl}_e}_d
+/g{{640 0 50 -221 611 533 _sc
+525 467 _m
+525 11 _l
+525 -63 504 -120 463 -160 _c
+422 -200 364 -221 288 -221 _c
+254 -221 221 -218 190 -212 _c
+158 -206 128 -196 100 -184 _c
+100 -75 _l
+147 -75 _l
+153 -109 166 -133 188 -149 _c
+210 -165 241 -173 282 -173 _c
+334 -173 373 -158 398 -128 _c
+422 -98 435 -51 435 11 _c
+435 81 _l
+417 48 395 24 368 9 _c
+340 -5 307 -13 267 -13 _c
+203 -13 150 12 110 62 _c
+70 112 50 178 50 260 _c
+50 341 70 407 110 457 _c
+150 507 203 533 267 533 _c
+307 533 340 525 368 510 _c
+395 494 417 470 435 438 _c
+435 519 _l
+611 519 _l
+611 467 _l
+525 467 _l
+435 285 _m
+}_e{435 347 423 394 399 427 _c
+375 460 340 477 295 477 _c
+249 477 214 458 190 422 _c
+166 386 155 332 155 260 _c
+155 188 166 133 190 97 _c
+214 60 249 42 295 42 _c
+340 42 375 58 399 91 _c
+423 123 435 171 435 234 _c
+435 285 _l
+_cl}_e}_d
+/h{{644 0 36 0 616 760 _sc
+41 0 _m
+41 52 _l
+122 52 _l
+122 708 _l
+36 708 _l
+36 760 _l
+212 760 _l
+212 427 _l
+228 461 250 488 276 506 _c
+302 524 333 533 369 533 _c
+426 533 468 516 495 484 _c
+522 451 536 400 536 330 _c
+536 52 _l
+616 52 _l
+616 0 _l
+368 0 _l
+368 52 _l
+446 52 _l
+446 302 _l
+446 365 438 408 422 432 _c
+406 455 379 467 340 467 _c
+298 467 266 451 244 421 _c
+222 391 212 347 212 289 _c
+212 52 _l
+290 52 _l
+290 0 _l
+41 0 _l
+_cl}_e}_d
+/i{320 0 36 0 297 736 _sc
+97 680 _m
+97 695 102 708 113 719 _c
+124 730 137 736 153 736 _c
+167 736 180 730 191 719 _c
+202 708 208 695 208 680 _c
+208 664 202 651 192 641 _c
+181 630 168 625 153 625 _c
+137 625 124 630 113 641 _c
+102 651 97 664 97 680 _c
+212 52 _m
+297 52 _l
+297 0 _l
+36 0 _l
+36 52 _l
+122 52 _l
+122 467 _l
+36 467 _l
+36 519 _l
+212 519 _l
+212 52 _l
+_cl}_d
+/l{320 0 29 0 290 760 _sc
+205 52 _m
+290 52 _l
+290 0 _l
+29 0 _l
+29 52 _l
+115 52 _l
+115 708 _l
+29 708 _l
+29 760 _l
+205 760 _l
+205 52 _l
+_cl}_d
+/m{{948 0 36 0 921 533 _sc
+518 418 _m
+535 456 557 484 584 504 _c
+611 523 642 533 678 533 _c
+732 533 773 516 800 482 _c
+826 448 840 398 840 330 _c
+840 52 _l
+921 52 _l
+921 0 _l
+672 0 _l
+672 52 _l
+750 52 _l
+750 320 _l
+750 372 742 410 726 433 _c
+710 455 685 467 649 467 _c
+609 467 578 451 557 421 _c
+536 391 526 347 526 289 _c
+526 52 _l
+604 52 _l
+604 0 _l
+358 0 _l
+358 52 _l
+436 52 _l
+436 323 _l
+436 375 428 412 412 434 _c
+396 456 371 467 335 467 _c
+295 467 264 451 243 421 _c
+222 391 212 347 212 289 _c
+212 52 _l
+}_e{290 52 _l
+290 0 _l
+41 0 _l
+41 52 _l
+122 52 _l
+122 468 _l
+36 468 _l
+36 519 _l
+212 519 _l
+212 427 _l
+228 461 249 488 275 506 _c
+301 524 330 533 363 533 _c
+403 533 436 523 463 503 _c
+490 483 508 454 518 418 _c
+_cl}_e}_d
+/n{{644 0 36 0 616 533 _sc
+41 0 _m
+41 52 _l
+122 52 _l
+122 467 _l
+36 467 _l
+36 519 _l
+212 519 _l
+212 427 _l
+228 461 250 488 276 506 _c
+302 524 333 533 369 533 _c
+426 533 468 516 495 484 _c
+522 451 536 400 536 330 _c
+536 52 _l
+616 52 _l
+616 0 _l
+368 0 _l
+368 52 _l
+446 52 _l
+446 302 _l
+446 365 438 408 422 432 _c
+406 456 379 468 340 468 _c
+298 468 266 452 244 422 _c
+222 391 212 347 212 289 _c
+212 52 _l
+290 52 _l
+290 0 _l
+41 0 _l
+_cl}_e}_d
+/o{602 0 50 -13 552 533 _sc
+301 34 _m
+349 34 385 53 410 91 _c
+434 129 447 185 447 260 _c
+447 334 434 390 410 428 _c
+385 466 349 485 301 485 _c
+253 485 216 466 192 428 _c
+167 390 155 334 155 260 _c
+155 185 167 129 192 91 _c
+216 53 253 34 301 34 _c
+301 -13 _m
+225 -13 165 11 119 61 _c
+73 111 50 177 50 260 _c
+50 342 72 408 118 458 _c
+164 508 225 533 301 533 _c
+377 533 437 508 483 458 _c
+529 408 552 342 552 260 _c
+552 177 529 111 483 61 _c
+437 11 377 -13 301 -13 _c
+_cl}_d
+/q{{640 0 50 -207 611 533 _sc
+525 467 _m
+525 -155 _l
+611 -155 _l
+611 -207 _l
+350 -207 _l
+350 -155 _l
+435 -155 _l
+435 81 _l
+417 48 395 24 368 9 _c
+340 -5 307 -13 267 -13 _c
+203 -13 150 12 110 62 _c
+70 112 50 178 50 260 _c
+50 341 70 407 110 457 _c
+150 507 203 533 267 533 _c
+307 533 340 525 368 510 _c
+395 494 417 470 435 438 _c
+435 519 _l
+611 519 _l
+611 467 _l
+525 467 _l
+435 285 _m
+435 347 423 394 399 427 _c
+375 460 340 477 295 477 _c
+249 477 214 458 190 422 _c
+166 386 155 332 155 260 _c
+155 188 166 133 190 97 _c
+214 60 249 42 295 42 _c
+}_e{340 42 375 58 399 91 _c
+423 123 435 171 435 234 _c
+435 285 _l
+_cl}_e}_d
+/r{478 0 36 0 478 533 _sc
+478 520 _m
+478 390 _l
+426 390 _l
+424 416 417 435 405 448 _c
+392 460 373 467 349 467 _c
+305 467 271 451 247 421 _c
+223 390 212 346 212 289 _c
+212 52 _l
+316 52 _l
+316 0 _l
+41 0 _l
+41 52 _l
+122 52 _l
+122 468 _l
+36 468 _l
+36 519 _l
+212 519 _l
+212 427 _l
+229 463 251 489 279 507 _c
+307 524 341 533 381 533 _c
+395 533 411 531 427 529 _c
+443 527 460 524 478 520 _c
+_cl}_d
+/s{{513 0 56 -13 462 533 _sc
+56 29 _m
+56 150 _l
+108 150 _l
+109 111 121 82 144 63 _c
+167 43 201 34 246 34 _c
+286 34 317 41 338 57 _c
+359 72 370 94 370 123 _c
+370 145 362 164 347 178 _c
+331 192 299 207 249 223 _c
+184 245 _l
+139 259 107 277 87 299 _c
+67 320 57 347 57 381 _c
+57 428 74 465 109 492 _c
+144 519 192 533 254 533 _c
+281 533 310 529 340 522 _c
+370 515 402 505 434 491 _c
+434 378 _l
+382 378 _l
+380 411 369 437 347 456 _c
+325 475 295 485 257 485 _c
+219 485 190 478 171 465 _c
+151 451 142 431 142 405 _c
+142 383 149 365 164 352 _c
+178 339 208 326 252 312 _c
+323 290 _l
+372 274 407 255 429 232 _c
+451 209 462 180 462 144 _c
+}_e{462 94 443 56 405 28 _c
+367 0 316 -13 250 -13 _c
+216 -13 184 -9 152 -3 _c
+120 3 88 14 56 29 _c
+_cl}_e}_d
+/t{402 0 29 -13 394 680 _sc
+108 467 _m
+29 467 _l
+29 519 _l
+108 519 _l
+108 680 _l
+198 680 _l
+198 519 _l
+367 519 _l
+367 467 _l
+198 467 _l
+198 137 _l
+198 93 202 64 211 52 _c
+219 40 235 34 258 34 _c
+281 34 298 41 309 55 _c
+319 69 325 91 326 122 _c
+394 122 _l
+391 74 378 40 355 19 _c
+332 -2 297 -13 250 -13 _c
+198 -13 161 -1 140 21 _c
+118 43 108 82 108 137 _c
+108 467 _l
+_cl}_d
+/u{{644 0 27 -13 607 519 _sc
+354 519 _m
+522 519 _l
+522 52 _l
+607 52 _l
+607 0 _l
+432 0 _l
+432 92 _l
+415 57 393 31 367 13 _c
+341 -4 310 -13 276 -13 _c
+218 -13 175 3 148 35 _c
+121 67 108 119 108 189 _c
+108 467 _l
+27 467 _l
+27 519 _l
+198 519 _l
+198 217 _l
+198 153 205 110 221 87 _c
+237 63 264 52 304 52 _c
+346 52 377 67 399 98 _c
+421 128 432 173 432 231 _c
+432 467 _l
+354 467 _l
+354 519 _l
+_cl}_e}_d
+/v{565 0 -2 0 562 519 _sc
+247 0 _m
+56 467 _l
+-2 467 _l
+-2 519 _l
+236 519 _l
+236 467 _l
+153 467 _l
+299 110 _l
+445 467 _l
+367 467 _l
+367 519 _l
+562 519 _l
+562 467 _l
+504 467 _l
+313 0 _l
+247 0 _l
+_cl}_d
+/x{{564 0 12 0 552 519 _sc
+291 317 _m
+400 467 _l
+330 467 _l
+330 519 _l
+530 519 _l
+530 467 _l
+461 467 _l
+322 275 _l
+484 52 _l
+552 52 _l
+552 0 _l
+312 0 _l
+312 52 _l
+378 52 _l
+265 207 _l
+152 52 _l
+219 52 _l
+219 0 _l
+22 0 _l
+22 52 _l
+91 52 _l
+234 249 _l
+76 467 _l
+12 467 _l
+12 519 _l
+244 519 _l
+244 467 _l
+182 467 _l
+291 317 _l
+_cl}_e}_d
+/y{{565 0 -2 -221 562 519 _sc
+216 -94 _m
+250 -8 _l
+56 467 _l
+-2 467 _l
+-2 519 _l
+236 519 _l
+236 467 _l
+153 467 _l
+299 110 _l
+445 467 _l
+367 467 _l
+367 519 _l
+562 519 _l
+562 467 _l
+504 467 _l
+266 -116 _l
+250 -156 232 -184 212 -199 _c
+192 -213 164 -221 128 -221 _c
+112 -221 97 -219 81 -217 _c
+65 -214 48 -210 32 -206 _c
+32 -107 _l
+78 -107 _l
+80 -129 85 -144 95 -154 _c
+104 -164 118 -169 138 -169 _c
+156 -169 170 -164 181 -154 _c
+192 -144 204 -124 216 -94 _c
+_cl}_e}_d
+end readonly def
+
+/BuildGlyph
+ {exch begin
+ CharStrings exch
+ 2 copy known not{pop /.notdef}if
+ true 3 1 roll get exec
+ end}_d
+
+/BuildChar {
+ 1 index /Encoding get exch get
+ 1 index /BuildGlyph get exec
+}_d
+
+FontName currentdict end definefont pop
+%% Font Page 00
+/BitstreamVeraSerif-Roman-ENC-00 [
+/.notdef/space/colon/C/l/i/e/n/t/S/o/r/a/g/R/s/u/c/h/P/v/d/E/x/D/B/I/m/q/y/M
+/A/f/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef
+/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef/.notdef] def
+/BitstreamVeraSerif-Roman-Uni-00 BitstreamVeraSerif-Roman-ENC-00 /BitstreamVeraSerif-Roman MFEmb
+%%BeginFont: BitstreamVeraSerif-Roman
+%!PS-AdobeFont-1.0 Composite Font
+%%FontName: BitstreamVeraSerif-Roman-Uni
+%%Creator: Composite font created by Qt
+25 dict begin
+/FontName /BitstreamVeraSerif-Roman-Uni def
+/PaintType 0 def
+/FontMatrix[1 0 0 1 0 0]def
+/FontType 0 def
+/FMapType 2 def
+/Encoding [
+0]def
+/FDepVector [
+/BitstreamVeraSerif-Roman-Uni-00 findfont
+]def
+FontName currentdict end definefont pop
+%%EndFont
+%%EndFont
+/F1 8.33333/BitstreamVeraSerif-Roman-Uni DF
+%%EndSetup
+%%Page: 1 1
+%%BeginPageSetup
+QI
+%%EndPageSetup
+[1 0 0 1 -39 -48]ST
+0 0 B 0 0 PE
+WB
+W BC
+39 48 630 215 R
+2 0 255 0 0 0 0 PE
+NB
+72 263 69 VL
+353 263 69 VL
+500 163 69 VL
+228 195 69 VL
+615 159 69 VL
+255 0 0 P1
+222 87 226 90 DL
+222 93 226 90 DL
+226 90 73 HL
+0 0 B 0 0 PE
+WB
+491 111 17 32 R
+255 0 0 P1
+NB
+491 111 17 32 R
+491 115 353 HL
+1 255 0 0 BR
+NP
+490 115 MT
+486 112 LT
+486 118 LT
+CP BF QS
+357 137 353 140 DL
+357 143 353 140 DL
+3 0 255 0 0 0 0 PE
+491 140 353 HL
+255 0 0 P1
+NB
+77 168 73 171 DL
+77 174 73 171 DL
+226 171 73 HL
+0 0 B 0 0 PE
+WB
+344 112 17 32 R
+255 0 0 P1
+NB
+344 112 17 32 R
+344 116 228 HL
+1 255 0 0 BR
+NP
+343 116 MT
+339 113 LT
+339 119 LT
+CP BF QS
+232 138 228 141 DL
+232 144 228 141 DL
+3 0 255 0 0 0 0 PE
+344 141 228 HL
+255 0 0 P1
+NB
+609 112 613 115 DL
+609 118 613 115 DL
+613 115 508 HL
+255 0 0 P1
+505 132 501 135 DL
+505 138 501 135 DL
+613 135 501 HL
+255 0 0 P1
+358 132 354 135 DL
+358 138 354 135 DL
+491 135 354 HL
+0 0 B 0 0 PE
+WB
+344 211 17 32 R
+255 0 0 P1
+NB
+344 211 17 32 R
+344 215 72 HL
+1 255 0 0 BR
+NP
+343 215 MT
+339 212 LT
+339 218 LT
+CP BF QS
+76 237 72 240 DL
+76 243 72 240 DL
+3 0 255 0 0 0 0 PE
+344 240 72 HL
+255 0 0 P1
+1 255 255 192 BR
+39 48 66 21 R
+CLSTART
+5 5 56 11 ACR
+CLEND
+B P1
+F1 F
+61 Y<000100020001000300040005000600070008>[3 0 3 0 3 0 6 0 3 0 3 0 5 0 5 0 0 0]34 55 63 1 Tl XYT
+CLO
+[1 0 0 1 -39 -48]ST
+255 0 0 P1
+1 255 255 192 BR
+328 48 51 21 R
+CLO
+[1 0 0 1 -39 -48]ST
+CLSTART
+294 5 41 11 ACR
+CLEND
+B P1
+1 255 255 192 BR
+F1 F
+<00010002000100090008000A000B000C000D0006>[3 0 3 0 3 0 5 0 3 0 5 0 4 0 5 0 5 0 0 0]41 333 63 1 Tl XYT
+CLO
+[1 0 0 1 -39 -48]ST
+255 0 0 P1
+1 255 255 192 BR
+472 48 57 21 R
+CLO
+[1 0 0 1 -39 -48]ST
+CLSTART
+438 5 47 11 ACR
+CLEND
+B P1
+1 255 255 192 BR
+F1 F
+<000100020001000E0006000F000A0010000B00110006>[3 0 3 0 3 0 6 0 5 0 4 0 5 0 5 0 4 0 4 0 0 0]47 477 63 1 Tl XYT
+CLO
+[1 0 0 1 -39 -48]ST
+255 0 0 P1
+1 255 255 192 BR
+186 48 85 21 R
+CLO
+[1 0 0 1 -39 -48]ST
+CLSTART
+152 5 75 11 ACR
+CLEND
+B P1
+1 255 255 192 BR
+F1 F
+<00010002000100090006000C000B0011001200010013000B000A0014000500150006000B>[3 0 3 0 3 0 5 0 5 0 5 0 4 0 4 0 5 0 3 0 5 0 4 0 5 0 4 0 3 0 5 0 5 0 0 0]75 191 63 1 Tl XYT
+CLO
+[1 0 0 1 -39 -48]ST
+255 0 0 P1
+1 255 255 192 BR
+562 48 107 21 R
+CLO
+[1 0 0 1 -39 -48]ST
+CLSTART
+528 5 97 11 ACR
+CLEND
+B P1
+1 255 255 192 BR
+F1 F
+<0001000200010016001700080006000B0007000C000400010018000C0008000C00010009000A0010000B00110006>[3 0 3 0 3 0 6 0 4 0 3 0 5 0 4 0 5 0 5 0 3 0 3 0 6 0 5 0 3 0 5 0 3 0 5 0 5 0 5 0 4 0 4 0 0 0]97 567 63 1 Tl XYT
+CLO
+[1 0 0 1 -39 -48]ST
+CLSTART
+39 23 102 15 ACR
+CLEND
+50 d2 P1
+NB
+F1 F
+81 Y<001800190010000F00010011000C0004000400020001000F0006000C000B00110012001A00080006001B000F>[6 0 6 0 5 0 4 0 3 0 4 0 5 0 3 0 3 0 3 0 3 0 4 0 5 0 5 0 4 0 4 0 5 0 3 0 3 0 5 0 7 0 0 0]94 82 XYT
+CLO
+[1 0 0 1 -39 -48]ST
+CLSTART
+319 48 136 15 ACR
+CLEND
+50 d2 P1
+NB
+F1 F
+106 Y<001800190010000F00010011000C0004000400020001000B0006001C00100006000F0008001A00080006001B001800060004000500140006000B001D>[6 0 6 0 5 0 4 0 3 0 4 0 5 0 3 0 3 0 3 0 3 0 4 0 5 0 5 0 5 0 5 0 4 0 3 0 3 0 3 0 5 0 7 0 6 0 5 0 3 0 3 0 4 0 5 0 4 0 0 0]128 362 XYT
+CLO
+[1 0 0 1 -39 -48]ST
+CLSTART
+39 104 138 15 ACR
+CLEND
+50 d2 P1
+NB
+F1 F
+162 Y<001800190010000F0001000F0005000D0007000C000400020001000F0006000C000B00110012001A00080006001B000F000E0006000F001000040008>[6 0 6 0 5 0 4 0 3 0 4 0 3 0 5 0 5 0 5 0 3 0 3 0 3 0 4 0 5 0 5 0 4 0 4 0 5 0 3 0 3 0 5 0 7 0 4 0 6 0 5 0 4 0 5 0 3 0 0 0]130 82 XYT
+CLO
+[1 0 0 1 -39 -48]ST
+CLSTART
+198 49 74 15 ACR
+CLEND
+50 d2 P1
+NB
+F1 F
+107 Y<001A001E001F001300010011000C000400040002000100200006000800110012>[3 0 8 0 6 0 5 0 3 0 4 0 5 0 3 0 3 0 3 0 3 0 3 0 5 0 3 0 4 0 0 0]66 241 XYT
+CLO
+[1 0 0 1 -39 -48]ST
+CLSTART
+467 48 92 15 ACR
+CLEND
+50 d2 P1
+NB
+F1 F
+106 Y<001F001100080005000A000700020001000B0006001C00100006000F000800010015000C0008000C>[6 0 4 0 3 0 3 0 5 0 5 0 3 0 3 0 4 0 5 0 5 0 5 0 5 0 4 0 3 0 3 0 5 0 5 0 3 0 0 0]84 510 XYT
+CLO
+[1 0 0 1 -39 -48]ST
+CLSTART
+467 68 94 15 ACR
+CLEND
+50 d2 P1
+NB
+F1 F
+126 Y<001F001100080005000A000700020001000B00060008000B000500060014000600010015000C0008000C>[6 0 4 0 3 0 3 0 5 0 5 0 3 0 3 0 4 0 5 0 3 0 4 0 3 0 5 0 4 0 5 0 3 0 5 0 5 0 3 0 0 0]86 510 XYT
+CLO
+[1 0 0 1 -39 -48]ST
+CLSTART
+320 68 75 15 ACR
+CLEND
+50 d2 P1
+NB
+F1 F
+<001A001E001F001300010011000C0004000400020001000F0008000A000B0006>[3 0 8 0 6 0 5 0 3 0 4 0 5 0 3 0 3 0 3 0 3 0 4 0 3 0 5 0 4 0 0 0]67 363 XYT
+CLO
+[1 0 0 1 -39 -48]ST
+CLSTART
+38 148 74 15 ACR
+CLEND
+50 d2 P1
+NB
+F1 F
+206 Y<001A001E001F001300010011000C000400040002000100200006000800110012>[3 0 8 0 6 0 5 0 3 0 4 0 5 0 3 0 3 0 3 0 3 0 3 0 5 0 3 0 4 0 0 0]66 81 XYT
+
+QP
+%%Trailer
+%%Pages: 1
+%%DocumentFonts: BitstreamVeraSerif-Roman
+%%EOF
diff --git a/doc/pics/akonadi_client_search.png b/doc/pics/akonadi_client_search.png
new file mode 100644 (file)
index 0000000..1c5915a
Binary files /dev/null and b/doc/pics/akonadi_client_search.png differ
diff --git a/doc/pics/akonadi_client_search_small.png b/doc/pics/akonadi_client_search_small.png
new file mode 100644 (file)
index 0000000..c089a30
Binary files /dev/null and b/doc/pics/akonadi_client_search_small.png differ
diff --git a/doc/pics/akonadi_communication.xmi b/doc/pics/akonadi_communication.xmi
new file mode 100644 (file)
index 0000000..ba88a2a
--- /dev/null
@@ -0,0 +1,185 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<XMI xmlns:UML="http://schema.omg.org/spec/UML/1.3" verified="false" timestamp="2006-09-30T19:30:24" xmi.version="1.2" >
+ <XMI.header>
+  <XMI.documentation>
+   <XMI.exporter>umbrello uml modeller http://uml.sf.net</XMI.exporter>
+   <XMI.exporterVersion>1.5.3</XMI.exporterVersion>
+   <XMI.exporterEncoding>UnicodeUTF8</XMI.exporterEncoding>
+  </XMI.documentation>
+  <XMI.metamodel xmi.name="UML" href="UML.xml" xmi.version="1.3" />
+ </XMI.header>
+ <XMI.content>
+  <UML:Model isSpecification="false" isLeaf="false" isRoot="false" xmi.id="m1" isAbstract="false" name="UML Model" >
+   <UML:Namespace.ownedElement>
+    <UML:Class isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="3" isRoot="false" isAbstract="false" name="Client" />
+    <UML:Class isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="4" isRoot="false" isAbstract="false" name="Search Provider" />
+    <UML:Class isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="5" isRoot="false" isAbstract="false" name="Storage" />
+    <UML:Class isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="6" isRoot="false" isAbstract="false" name="Resource" />
+    <UML:Class isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="22" isRoot="false" isAbstract="false" name="AgentManager (libakonadi)" />
+    <UML:Class isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="23" isRoot="false" isAbstract="false" name="ProfileManager (libakonadi)" />
+    <UML:Class isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="26" isRoot="false" isAbstract="false" name="Control" />
+    <UML:Class isSpecification="false" isLeaf="false" visibility="public" namespace="m1" xmi.id="44" isRoot="false" isAbstract="false" name="External Data Source" />
+   </UML:Namespace.ownedElement>
+  </UML:Model>
+ </XMI.content>
+ <XMI.extensions xmi.extender="umbrello" >
+  <docsettings viewid="54" documentation="" uniqueid="81" />
+  <diagrams>
+   <diagram snapgrid="0" showattsig="1" fillcolor="#ffffc0" linewidth="0" zoom="100" showgrid="0" showopsig="1" usefillcolor="1" snapx="10" canvaswidth="843" snapy="10" showatts="1" xmi.id="2" documentation="" type="403" showops="1" showpackage="0" name="Akonadi Client Search" localid="899994" showstereotype="0" showscope="1" snapcsgrid="0" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="#ff0000" canvasheight="556" >
+    <widgets>
+     <objectwidget usesdiagramfillcolour="0" width="56" usesdiagramusefillcolour="1" x="34" y="48" instancename="" linewidth="none" fillcolour="#ffffc0" height="25" usefillcolor="1" isinstance="0" xmi.id="3" decon="0" localid="899999" multipleinstance="0" drawasactor="0" font="Serif,8,-1,5,50,0,1,0,0,0" linecolor="#ff0000" />
+     <objectwidget usesdiagramfillcolour="0" width="67" usesdiagramusefillcolour="1" x="354" y="48" instancename="" linewidth="none" fillcolour="#ffffc0" height="25" usefillcolor="1" isinstance="0" xmi.id="5" decon="0" localid="899997" multipleinstance="0" drawasactor="0" font="Serif,8,-1,5,50,0,1,0,0,0" linecolor="#ff0000" />
+     <objectwidget usesdiagramfillcolour="0" width="75" usesdiagramusefillcolour="1" x="541" y="48" instancename="" linewidth="none" fillcolour="#ffffc0" height="25" usefillcolor="1" isinstance="0" xmi.id="6" decon="0" localid="899996" multipleinstance="0" drawasactor="0" showstereotype="1" font="Serif,8,-1,5,50,0,1,0,0,0" linecolor="#ff0000" />
+     <objectwidget usesdiagramfillcolour="0" width="114" usesdiagramusefillcolour="1" x="204" y="48" instancename="" linewidth="none" fillcolour="#ffffc0" height="25" usefillcolor="1" isinstance="0" xmi.id="4" decon="0" localid="899995" multipleinstance="0" drawasactor="0" showstereotype="1" font="Serif,8,-1,5,50,0,1,0,0,0" linecolor="#ff0000" />
+     <objectwidget usesdiagramfillcolour="0" width="145" usesdiagramusefillcolour="1" x="687" y="48" instancename="" linewidth="none" fillcolour="#ffffc0" height="25" usefillcolor="1" isinstance="0" xmi.id="44" decon="0" localid="899994" multipleinstance="0" drawasactor="0" font="Serif,8,-1,5,50,0,1,0,0,0" linecolor="#ff0000" />
+    </widgets>
+    <messages>
+     <messagewidget usesdiagramfillcolour="1" width="197" usesdiagramusefillcolour="1" x="63" y="96" operation="searchItems" linewidth="none" widgetbid="899995" fillcolour="none" height="8" usefillcolor="1" seqnum="DBus call" textid="11" widgetaid="899999" isinstance="0" xmi.id="10" sequencemessagetype="1001" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="140" usesdiagramusefillcolour="1" x="68" y="77" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="11" showstereotype="1" text="DBus call: searchItems" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="199" usesdiagramusefillcolour="1" x="387" y="111" operation="requestItemDelivery" linewidth="none" widgetbid="899996" fillcolour="none" height="32" usefillcolor="1" seqnum="DBus call" textid="15" widgetaid="899997" isinstance="0" xmi.id="14" sequencemessagetype="1000" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="187" usesdiagramusefillcolour="1" x="392" y="92" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="15" showstereotype="1" text="DBus call: requestItemDelivery" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="197" usesdiagramusefillcolour="1" x="63" y="167" operation="searchItemsResult" linewidth="none" widgetbid="899999" fillcolour="none" height="8" usefillcolor="1" seqnum="DBus signal" textid="19" widgetaid="899995" isinstance="0" xmi.id="18" sequencemessagetype="1001" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="190" usesdiagramusefillcolour="1" x="68" y="148" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="19" showstereotype="1" text="DBus signal: searchItemsResult" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="134" usesdiagramusefillcolour="1" x="261" y="112" operation="fetch" linewidth="none" widgetbid="899997" fillcolour="none" height="32" usefillcolor="1" seqnum="IMAP call" textid="43" widgetaid="899995" isinstance="0" xmi.id="42" sequencemessagetype="1000" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="99" usesdiagramusefillcolour="1" x="266" y="93" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="43" showstereotype="1" text="IMAP call: fetch" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="179" usesdiagramusefillcolour="1" x="579" y="111" operation="request data" linewidth="none" widgetbid="899994" fillcolour="none" height="8" usefillcolor="1" seqnum="Action" textid="46" widgetaid="899996" isinstance="0" xmi.id="45" sequencemessagetype="1001" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="124" usesdiagramusefillcolour="1" x="584" y="92" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="46" text="Action: request data" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="179" usesdiagramusefillcolour="1" x="579" y="131" operation="retrieve data" linewidth="none" widgetbid="899996" fillcolour="none" height="8" usefillcolor="1" seqnum="Action" textid="48" widgetaid="899994" isinstance="0" xmi.id="47" sequencemessagetype="1001" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="126" usesdiagramusefillcolour="1" x="584" y="112" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="48" showstereotype="1" text="Action: retrieve data" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="189" usesdiagramusefillcolour="1" x="388" y="131" operation="store" linewidth="none" widgetbid="899997" fillcolour="none" height="8" usefillcolor="1" seqnum="IMAP call" textid="50" widgetaid="899996" isinstance="0" xmi.id="49" sequencemessagetype="1001" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="100" usesdiagramusefillcolour="1" x="396" y="112" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="50" showstereotype="1" text="IMAP call: store" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="333" usesdiagramusefillcolour="1" x="62" y="211" operation="fetch" linewidth="none" widgetbid="899997" fillcolour="none" height="32" usefillcolor="1" seqnum="IMAP call" textid="52" widgetaid="899999" isinstance="0" xmi.id="51" sequencemessagetype="1000" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="99" usesdiagramusefillcolour="1" x="77" y="192" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="52" showstereotype="1" text="IMAP call: fetch" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+    </messages>
+    <associations/>
+   </diagram>
+   <diagram snapgrid="0" showattsig="1" fillcolor="#ffffc0" linewidth="0" zoom="100" showgrid="0" showopsig="1" usefillcolor="1" snapx="10" canvaswidth="843" snapy="10" showatts="1" xmi.id="21" documentation="" type="403" showops="1" showpackage="0" name="Akonadi Profile Handling" localid="899996" showstereotype="0" showscope="1" snapcsgrid="0" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="#ff0000" canvasheight="556" >
+    <widgets>
+     <objectwidget usesdiagramfillcolour="0" width="184" usesdiagramusefillcolour="1" x="179" y="48" instancename="" linewidth="none" fillcolour="#ffffc0" height="25" usefillcolor="1" isinstance="0" xmi.id="23" decon="0" localid="899998" multipleinstance="0" drawasactor="0" showstereotype="1" font="Serif,8,-1,5,50,0,1,0,0,0" linecolor="#ff0000" />
+     <objectwidget usesdiagramfillcolour="0" width="56" usesdiagramusefillcolour="0" x="61" y="48" instancename="" linewidth="none" fillcolour="#ffffc0" height="25" usefillcolor="1" isinstance="0" xmi.id="3" decon="0" localid="899997" multipleinstance="0" drawasactor="0" showstereotype="1" font="Serif,8,-1,5,50,0,1,0,0,0" linecolor="#ff0000" />
+     <objectwidget usesdiagramfillcolour="0" width="64" usesdiagramusefillcolour="1" x="385" y="48" instancename="" linewidth="none" fillcolour="#ffffc0" height="25" usefillcolor="1" isinstance="0" xmi.id="26" decon="0" localid="899996" multipleinstance="0" drawasactor="0" font="Serif,8,-1,5,50,0,1,0,0,0" linecolor="#ff0000" />
+    </widgets>
+    <messages>
+     <messagewidget usesdiagramfillcolour="1" width="190" usesdiagramusefillcolour="1" x="94" y="100" operation="createProfile" linewidth="none" widgetbid="899998" fillcolour="none" height="32" usefillcolor="1" seqnum="Method call" textid="25" widgetaid="899997" isinstance="0" xmi.id="24" sequencemessagetype="1000" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="158" usesdiagramusefillcolour="1" x="99" y="81" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="25" showstereotype="1" text="Method call: createProfile" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="154" usesdiagramusefillcolour="1" x="247" y="100" operation="createProfile" linewidth="none" widgetbid="899996" fillcolour="none" height="32" usefillcolor="1" seqnum="DBus call" textid="28" widgetaid="899998" isinstance="0" xmi.id="27" sequencemessagetype="1000" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="145" usesdiagramusefillcolour="1" x="252" y="81" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="28" showstereotype="1" text="DBus call: createProfile" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+    </messages>
+    <associations/>
+   </diagram>
+   <diagram snapgrid="0" showattsig="1" fillcolor="#ffffc0" linewidth="0" zoom="100" showgrid="0" showopsig="1" usefillcolor="1" snapx="10" canvaswidth="843" snapy="10" showatts="1" xmi.id="29" documentation="" type="403" showops="1" showpackage="0" name="Akonadi Agent Handling" localid="899996" showstereotype="0" showscope="1" snapcsgrid="0" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="#ff0000" canvasheight="556" >
+    <widgets>
+     <objectwidget usesdiagramfillcolour="0" width="56" usesdiagramusefillcolour="1" x="42" y="48" instancename="" linewidth="none" fillcolour="#ffffc0" height="25" usefillcolor="1" isinstance="0" xmi.id="3" decon="0" localid="899999" multipleinstance="0" drawasactor="0" showstereotype="1" font="Serif,8,-1,5,50,0,1,0,0,0" linecolor="#ff0000" />
+     <objectwidget usesdiagramfillcolour="0" width="179" usesdiagramusefillcolour="1" x="186" y="48" instancename="" linewidth="none" fillcolour="#ffffc0" height="25" usefillcolor="1" isinstance="0" xmi.id="22" decon="0" localid="899998" multipleinstance="0" drawasactor="0" showstereotype="1" font="Serif,8,-1,5,50,0,1,0,0,0" linecolor="#ff0000" />
+     <objectwidget usesdiagramfillcolour="0" width="64" usesdiagramusefillcolour="1" x="407" y="48" instancename="" linewidth="none" fillcolour="#ffffc0" height="25" usefillcolor="1" isinstance="0" xmi.id="26" decon="0" localid="899997" multipleinstance="0" drawasactor="0" showstereotype="1" font="Serif,8,-1,5,50,0,1,0,0,0" linecolor="#ff0000" />
+     <objectwidget usesdiagramfillcolour="0" width="75" usesdiagramusefillcolour="1" x="582" y="48" instancename="" linewidth="none" fillcolour="#ffffc0" height="25" usefillcolor="1" isinstance="0" xmi.id="6" decon="0" localid="899996" multipleinstance="0" drawasactor="0" showstereotype="1" font="Serif,8,-1,5,50,0,1,0,0,0" linecolor="#ff0000" />
+    </widgets>
+    <messages>
+     <messagewidget usesdiagramfillcolour="1" width="213" usesdiagramusefillcolour="1" x="67" y="100" operation="createAgentInstance" linewidth="none" widgetbid="899998" fillcolour="none" height="32" usefillcolor="1" seqnum="Method call" textid="31" widgetaid="899999" isinstance="0" xmi.id="30" sequencemessagetype="1000" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="201" usesdiagramusefillcolour="1" x="72" y="81" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="31" text="Method call: createAgentInstance" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="172" usesdiagramusefillcolour="1" x="252" y="101" operation="createAgentInstance" linewidth="none" widgetbid="899997" fillcolour="none" height="32" usefillcolor="1" seqnum="DBus call" textid="33" widgetaid="899998" isinstance="0" xmi.id="32" sequencemessagetype="1000" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="188" usesdiagramusefillcolour="1" x="257" y="82" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="33" showstereotype="1" text="DBus call: createAgentInstance" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="178" usesdiagramusefillcolour="1" x="433" y="109" operation="starts the resource" linewidth="none" widgetbid="899996" fillcolour="none" height="8" usefillcolor="1" seqnum="Action" textid="35" widgetaid="899997" isinstance="0" xmi.id="34" sequencemessagetype="1001" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="160" usesdiagramusefillcolour="1" x="441" y="90" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="35" showstereotype="1" text="Action: starts the resource" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="178" usesdiagramusefillcolour="1" x="433" y="161" operation="serviceOwnerChanged" linewidth="none" widgetbid="899997" fillcolour="none" height="8" usefillcolor="1" seqnum="DBus signal" textid="37" widgetaid="899996" isinstance="0" xmi.id="36" sequencemessagetype="1001" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="213" usesdiagramusefillcolour="1" x="438" y="142" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="37" showstereotype="1" text="DBus signal: serviceOwnerChanged" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="162" usesdiagramusefillcolour="1" x="253" y="174" operation="agentInstanceAdded" linewidth="none" widgetbid="899998" fillcolour="none" height="8" usefillcolor="1" seqnum="DBus signal" textid="39" widgetaid="899997" isinstance="0" xmi.id="38" sequencemessagetype="1001" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="201" usesdiagramusefillcolour="1" x="258" y="155" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="39" showstereotype="1" text="DBus signal: agentInstanceAdded" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="203" usesdiagramusefillcolour="1" x="68" y="188" operation="agentInstanceAdded" linewidth="none" widgetbid="899999" fillcolour="none" height="8" usefillcolor="1" seqnum="Qt signal" textid="41" widgetaid="899998" isinstance="0" xmi.id="40" sequencemessagetype="1001" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="184" usesdiagramusefillcolour="1" x="73" y="169" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="41" showstereotype="1" text="Qt signal: agentInstanceAdded" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+    </messages>
+    <associations/>
+   </diagram>
+   <diagram snapgrid="0" showattsig="1" fillcolor="#ffffc0" linewidth="0" zoom="100" showgrid="0" showopsig="1" usefillcolor="1" snapx="10" canvaswidth="843" snapy="10" showatts="1" xmi.id="54" documentation="" type="403" showops="1" showpackage="0" name="Akonadi Client Search II" localid="899995" showstereotype="0" showscope="1" snapcsgrid="0" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="#ff0000" canvasheight="556" >
+    <widgets>
+     <objectwidget usesdiagramfillcolour="0" width="56" usesdiagramusefillcolour="1" x="7" y="48" instancename="" linewidth="none" fillcolour="#ffffc0" height="25" usefillcolor="1" isinstance="0" xmi.id="3" decon="0" localid="899999" multipleinstance="0" drawasactor="0" font="Serif,8,-1,5,50,0,1,0,0,0" linecolor="#ff0000" />
+     <objectwidget usesdiagramfillcolour="0" width="67" usesdiagramusefillcolour="1" x="160" y="48" instancename="" linewidth="none" fillcolour="#ffffc0" height="25" usefillcolor="1" isinstance="0" xmi.id="5" decon="0" localid="899998" multipleinstance="0" drawasactor="0" font="Serif,8,-1,5,50,0,1,0,0,0" linecolor="#ff0000" />
+     <objectwidget usesdiagramfillcolour="0" width="114" usesdiagramusefillcolour="1" x="343" y="48" instancename="" linewidth="none" fillcolour="#ffffc0" height="25" usefillcolor="1" isinstance="0" xmi.id="4" decon="0" localid="899997" multipleinstance="0" drawasactor="0" font="Serif,8,-1,5,50,0,1,0,0,0" linecolor="#ff0000" />
+     <objectwidget usesdiagramfillcolour="0" width="75" usesdiagramusefillcolour="1" x="493" y="48" instancename="" linewidth="none" fillcolour="#ffffc0" height="25" usefillcolor="1" isinstance="0" xmi.id="6" decon="0" localid="899996" multipleinstance="0" drawasactor="0" showstereotype="1" font="Serif,8,-1,5,50,0,1,0,0,0" linecolor="#ff0000" />
+     <objectwidget usesdiagramfillcolour="0" width="145" usesdiagramusefillcolour="1" x="661" y="48" instancename="" linewidth="none" fillcolour="#ffffc0" height="25" usefillcolor="1" isinstance="0" xmi.id="44" decon="0" localid="899995" multipleinstance="0" drawasactor="0" showstereotype="1" font="Serif,8,-1,5,50,0,1,0,0,0" linecolor="#ff0000" />
+     <notewidget usesdiagramfillcolour="1" width="100" usesdiagramusefillcolour="1" x="85" y="188" linewidth="none" fillcolour="none" height="92" usefillcolor="1" isinstance="0" xmi.id="63" text="Request item from resource if not available in Storage" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+    </widgets>
+    <messages>
+     <messagewidget usesdiagramfillcolour="1" width="156" usesdiagramusefillcolour="1" x="36" y="100" operation="search" linewidth="none" widgetbid="899998" fillcolour="none" height="8" usefillcolor="1" seqnum="IMAP call" textid="56" widgetaid="899999" isinstance="0" xmi.id="55" sequencemessagetype="1001" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="109" usesdiagramusefillcolour="1" x="45" y="81" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="56" showstereotype="1" text="IMAP call: search" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="205" usesdiagramusefillcolour="1" x="194" y="122" operation="addSearch" linewidth="none" widgetbid="899997" fillcolour="none" height="8" usefillcolor="1" seqnum="DBus call" textid="58" widgetaid="899998" isinstance="0" xmi.id="57" sequencemessagetype="1001" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="132" usesdiagramusefillcolour="1" x="203" y="103" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="58" showstereotype="1" text="DBus call: addSearch" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="205" usesdiagramusefillcolour="1" x="194" y="161" operation="fetch" linewidth="none" widgetbid="899998" fillcolour="none" height="8" usefillcolor="1" seqnum="IMAP call" textid="60" widgetaid="899997" isinstance="0" xmi.id="59" sequencemessagetype="1001" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="99" usesdiagramusefillcolour="1" x="203" y="142" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="60" showstereotype="1" text="IMAP call: fetch" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="335" usesdiagramusefillcolour="1" x="194" y="227" operation="requestItemDelivery" linewidth="none" widgetbid="899996" fillcolour="none" height="8" usefillcolor="1" seqnum="DBus call" textid="62" widgetaid="899998" isinstance="0" xmi.id="61" sequencemessagetype="1001" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="187" usesdiagramusefillcolour="1" x="203" y="208" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="62" showstereotype="1" text="DBus call: requestItemDelivery" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="201" usesdiagramusefillcolour="1" x="531" y="251" operation="" linewidth="none" widgetbid="899995" fillcolour="none" height="8" usefillcolor="1" seqnum="Custom action" textid="65" widgetaid="899996" isinstance="0" xmi.id="64" sequencemessagetype="1001" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="97" usesdiagramusefillcolour="1" x="540" y="232" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="65" showstereotype="1" text="Custom action: " font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="201" usesdiagramusefillcolour="1" x="531" y="282" operation="" linewidth="none" widgetbid="899996" fillcolour="none" height="8" usefillcolor="1" seqnum="Custom signal" textid="67" widgetaid="899995" isinstance="0" xmi.id="66" sequencemessagetype="1001" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="97" usesdiagramusefillcolour="1" x="540" y="263" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="67" showstereotype="1" text="Custom signal: " font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="335" usesdiagramusefillcolour="1" x="194" y="305" operation="store" linewidth="none" widgetbid="899998" fillcolour="none" height="8" usefillcolor="1" seqnum="IMAP call" textid="69" widgetaid="899996" isinstance="0" xmi.id="68" sequencemessagetype="1001" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="100" usesdiagramusefillcolour="1" x="203" y="286" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="69" showstereotype="1" text="IMAP call: store" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="205" usesdiagramusefillcolour="1" x="194" y="342" operation="" linewidth="none" widgetbid="899997" fillcolour="none" height="8" usefillcolor="1" seqnum="Response" textid="71" widgetaid="899998" isinstance="0" xmi.id="70" sequencemessagetype="1001" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="71" usesdiagramusefillcolour="1" x="203" y="323" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="71" showstereotype="1" text="Response: " font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="205" usesdiagramusefillcolour="1" x="194" y="379" operation="store" linewidth="none" widgetbid="899998" fillcolour="none" height="8" usefillcolor="1" seqnum="IMAP call" textid="73" widgetaid="899997" isinstance="0" xmi.id="72" sequencemessagetype="1001" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="100" usesdiagramusefillcolour="1" x="206" y="360" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="73" showstereotype="1" text="IMAP call: store" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="156" usesdiagramusefillcolour="1" x="36" y="399" operation="" linewidth="none" widgetbid="899999" fillcolour="none" height="8" usefillcolor="1" seqnum="Response" textid="75" widgetaid="899998" isinstance="0" xmi.id="74" sequencemessagetype="1001" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="71" usesdiagramusefillcolour="1" x="43" y="380" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="75" showstereotype="1" text="Response: " font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="205" usesdiagramusefillcolour="1" x="194" y="434" operation="removeSearch" linewidth="none" widgetbid="899997" fillcolour="none" height="8" usefillcolor="1" seqnum="DBus call" textid="77" widgetaid="899998" isinstance="0" xmi.id="76" sequencemessagetype="1001" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="153" usesdiagramusefillcolour="1" x="201" y="415" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="77" showstereotype="1" text="DBus call: removeSearch" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="156" usesdiagramusefillcolour="1" x="36" y="458" operation="uidFetch" linewidth="none" widgetbid="899998" fillcolour="none" height="8" usefillcolor="1" seqnum="IMAP call" textid="79" widgetaid="899999" isinstance="0" xmi.id="78" sequencemessagetype="1001" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="121" usesdiagramusefillcolour="1" x="41" y="439" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="79" showstereotype="1" text="IMAP call: uidFetch" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+     <messagewidget usesdiagramfillcolour="1" width="156" usesdiagramusefillcolour="1" x="36" y="496" operation="" linewidth="none" widgetbid="899999" fillcolour="none" height="8" usefillcolor="1" seqnum="Response" textid="81" widgetaid="899998" isinstance="0" xmi.id="80" sequencemessagetype="1001" showstereotype="1" font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" >
+      <floatingtext usesdiagramfillcolour="1" width="71" usesdiagramusefillcolour="1" x="41" y="477" linewidth="none" posttext="" role="704" fillcolour="none" height="19" usefillcolor="1" pretext="" isinstance="0" xmi.id="81" showstereotype="1" text="Response: " font="Serif,8,-1,5,50,0,0,0,0,0" linecolor="none" />
+     </messagewidget>
+    </messages>
+    <associations/>
+   </diagram>
+  </diagrams>
+  <listview>
+   <listitem open="1" type="800" label="Views" >
+    <listitem open="1" type="801" label="Logical View" >
+     <listitem open="1" type="813" id="22" />
+     <listitem open="1" type="813" id="3" />
+     <listitem open="1" type="813" id="26" />
+     <listitem open="1" type="813" id="44" />
+     <listitem open="1" type="813" id="23" />
+     <listitem open="1" type="813" id="6" />
+     <listitem open="1" type="813" id="4" />
+     <listitem open="1" type="813" id="5" />
+     <listitem open="0" type="830" label="Datatypes" />
+    </listitem>
+    <listitem open="1" type="802" label="Use Case View" />
+    <listitem open="1" type="821" label="Component View" />
+    <listitem open="1" type="827" label="Deployment View" />
+    <listitem open="1" type="836" label="Entity Relationship Model" />
+   </listitem>
+  </listview>
+  <codegeneration>
+   <codegenerator language="XMLSchema" />
+  </codegeneration>
+ </XMI.extensions>
+</XMI>
diff --git a/doc/pics/akonadi_concept_schema.sla b/doc/pics/akonadi_concept_schema.sla
new file mode 100644 (file)
index 0000000..9ffc59b
--- /dev/null
@@ -0,0 +1,721 @@
+<SCRIBUSUTF8NEW Version="1.3.3.3" >
+ <DOCUMENT HalfRes="1" MAGMAX="800" TextPenShade="100" MAJGRID="100" ABSTSPALTEN="11" ScratchBottom="20" AUTOCHECK="0" LANGUAGE="German" DPIn2="" DPgam="0" HCMS="1" UnderlineWidth="-1" TabFill="" DGAP="0" ORIENTATION="1" PASPECT="1" WIDTH="1" POLYR="0" SHOWLINK="0" MINWORDLEN="3" UnderlinePos="-1" VTIEFSC="100" DOCLANGINFO="" COMMENTS="" AutoSaveTime="600000" POLYS="0" GuideRad="10" rulerMode="1" TITLE="" KEYWORDS="" TabWidth="36" DSIZE="12" AUTOSPALTEN="1" PAGESIZE="A6" STIL="1" TextBackGroundShade="100" PEN="Black" POLYC="4" SnapToGuides="0" GROUPC="59" DOCFORMAT="" DOCDATE="" BORDERTOP="9" currentProfile="Postscript" MARGC="#0000ff" EndArrow="0" SHOWBASE="0" SHOWGRID="0" SnapToGrid="0" GUIDELOCK="0" DIMo="1" DIPr="0" StrikeThruPos="-1" WIDTHLINE="1" TextStrokeShade="100" DPuse="0" DPSo="0" DOCSOURCE="" FIRSTNUM="1" GuideC="#000080" BRUSH="Black" StartArrow="0" ScratchRight="100" POLYF="0.5" SHOWMARGIN="1" DPbla="1" StrikeThruWidth="-1" VHOCHSC="100" DOCTYPE="" BORDERBOTTOM="9" BRUSHSHADE="100" StrokeText="Black" BASEGRID="12" VTIEF="33" DOCCONTRIB="" DOCRELATION="" PICTSCX="1" CPICT="White" PENLINE="Black" AutoSave="0" BASEO="0" DOCIDENT="" BOOK="0" PICTSCY="1" MAGSTEP="200" TextLineColor="None" ScratchTop="20" POLYFD="0" AUTOL="20" PUBLISHER="" ANZPAGES="1" PSCALE="1" LINESHADE="100" HYCOUNT="2" DIMo2="1" AUTHOR="" UNITS="0" BORDERRIGHT="9" RANDF="0" MAJORC="#00ff00" PENSHADE="100" PENTEXT="Black" GRAB="4" showcolborders="0" SHOWGUIDES="1" DPInCMYK="" DPPr="" DPMo="" PAGEHEIGHT="297.64" PAGEWIDTH="419.53" BACKG="1" GuideZ="10" TextBackGround="None" MINGRID="20" VHOCH="33" DOCCOVER="" DCOL="1" EmbeddedPath="0" rulerYoffset="0" SHOWPICT="1" SHOWFRAME="1" AUTOMATIC="1" ALAYER="0" DOCRIGHTS="" PICTSHADE="100" ScratchLeft="100" rulerXoffset="0" showrulers="1" DPIn="" VKAPIT="75" DFONT="Bitstream Charter Bold Italic" BORDERLEFT="9" PAGEC="#ffffff" BaseC="#c0c0c0" MINORC="#00ff00" MAGMIN="10" STILLINE="1" TextLineShade="100" SHOWControl="0" >
+  <CheckProfile checkTransparency="1" autoCheck="1" minResolution="72" checkOverflow="1" ignoreErrors="0" checkRasterPDF="1" checkResolution="1" checkGlyphs="1" Name="PDF 1.3" checkAnnotations="0" checkPictures="1" checkOrphans="1" />
+  <CheckProfile checkTransparency="0" autoCheck="1" minResolution="72" checkOverflow="1" ignoreErrors="0" checkRasterPDF="1" checkResolution="1" checkGlyphs="1" Name="PDF 1.4" checkAnnotations="0" checkPictures="1" checkOrphans="1" />
+  <CheckProfile checkTransparency="1" autoCheck="1" minResolution="144" checkOverflow="1" ignoreErrors="0" checkRasterPDF="1" checkResolution="1" checkGlyphs="1" Name="PDF/X-3" checkAnnotations="1" checkPictures="1" checkOrphans="1" />
+  <CheckProfile checkTransparency="1" autoCheck="1" minResolution="72" checkOverflow="1" ignoreErrors="0" checkRasterPDF="1" checkResolution="1" checkGlyphs="1" Name="PostScript" checkAnnotations="0" checkPictures="1" checkOrphans="1" />
+  <CheckProfile checkTransparency="1" autoCheck="1" minResolution="72" checkOverflow="1" ignoreErrors="0" checkRasterPDF="1" checkResolution="1" checkGlyphs="1" Name="Postscript" checkAnnotations="0" checkPictures="1" checkOrphans="1" />
+  <COLOR Register="0" Spot="0" CMYK="#0f070000" NAME="AliceBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#000f2305" NAME="AntiqueWhite" />
+  <COLOR Register="0" Spot="0" CMYK="#00102400" NAME="AntiqueWhite1" />
+  <COLOR Register="0" Spot="0" CMYK="#000f2211" NAME="AntiqueWhite2" />
+  <COLOR Register="0" Spot="0" CMYK="#000d1d32" NAME="AntiqueWhite3" />
+  <COLOR Register="0" Spot="0" CMYK="#00081374" NAME="AntiqueWhite4" />
+  <COLOR Register="0" Spot="0" CMYK="#80002b00" NAME="Aquamarine" />
+  <COLOR Register="0" Spot="0" CMYK="#80002b00" NAME="Aquamarine1" />
+  <COLOR Register="0" Spot="0" CMYK="#78002811" NAME="Aquamarine2" />
+  <COLOR Register="0" Spot="0" CMYK="#67002332" NAME="Aquamarine3" />
+  <COLOR Register="0" Spot="0" CMYK="#46001774" NAME="Aquamarine4" />
+  <COLOR Register="0" Spot="0" CMYK="#0f000000" NAME="Azure" />
+  <COLOR Register="0" Spot="0" CMYK="#0f000000" NAME="Azure1" />
+  <COLOR Register="0" Spot="0" CMYK="#0e000011" NAME="Azure2" />
+  <COLOR Register="0" Spot="0" CMYK="#0c000032" NAME="Azure3" />
+  <COLOR Register="0" Spot="0" CMYK="#08000074" NAME="Azure4" />
+  <COLOR Register="0" Spot="0" CMYK="#0000190a" NAME="Beige" />
+  <COLOR Register="0" Spot="0" CMYK="#001b3b00" NAME="Bisque" />
+  <COLOR Register="0" Spot="0" CMYK="#001b3b00" NAME="Bisque1" />
+  <COLOR Register="0" Spot="0" CMYK="#00193711" NAME="Bisque2" />
+  <COLOR Register="0" Spot="0" CMYK="#00162f32" NAME="Bisque3" />
+  <COLOR Register="0" Spot="0" CMYK="#000e2074" NAME="Bisque4" />
+  <COLOR Register="0" Spot="0" CMYK="#000000ff" NAME="Black" />
+  <COLOR Register="0" Spot="0" CMYK="#00143200" NAME="BlanchedAlmond" />
+  <COLOR Register="0" Spot="0" CMYK="#ffff0000" NAME="Blue" />
+  <COLOR Register="0" Spot="0" CMYK="#ffff0000" NAME="Blue1" />
+  <COLOR Register="0" Spot="0" CMYK="#eeee0011" NAME="Blue2" />
+  <COLOR Register="0" Spot="0" CMYK="#cdcd0032" NAME="Blue3" />
+  <COLOR Register="0" Spot="0" CMYK="#8b8b0074" NAME="Blue4" />
+  <COLOR Register="0" Spot="0" CMYK="#58b7001d" NAME="BlueViolet" />
+  <COLOR Register="0" Spot="0" CMYK="#007b7b5a" NAME="Brown" />
+  <COLOR Register="0" Spot="0" CMYK="#00bfbf00" NAME="Brown1" />
+  <COLOR Register="0" Spot="0" CMYK="#00b3b311" NAME="Brown2" />
+  <COLOR Register="0" Spot="0" CMYK="#009a9a32" NAME="Brown3" />
+  <COLOR Register="0" Spot="0" CMYK="#00686874" NAME="Brown4" />
+  <COLOR Register="0" Spot="0" CMYK="#00265721" NAME="Burlywood" />
+  <COLOR Register="0" Spot="0" CMYK="#002c6400" NAME="Burlywood1" />
+  <COLOR Register="0" Spot="0" CMYK="#00295d11" NAME="Burlywood2" />
+  <COLOR Register="0" Spot="0" CMYK="#00235032" NAME="Burlywood3" />
+  <COLOR Register="0" Spot="0" CMYK="#00183674" NAME="Burlywood4" />
+  <COLOR Register="0" Spot="0" CMYK="#4102005f" NAME="CadetBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#670a0000" NAME="CadetBlue1" />
+  <COLOR Register="0" Spot="0" CMYK="#60090011" NAME="CadetBlue2" />
+  <COLOR Register="0" Spot="0" CMYK="#53080032" NAME="CadetBlue3" />
+  <COLOR Register="0" Spot="0" CMYK="#38050074" NAME="CadetBlue4" />
+  <COLOR Register="0" Spot="0" CMYK="#8000ff00" NAME="Chartreuse" />
+  <COLOR Register="0" Spot="0" CMYK="#8000ff00" NAME="Chartreuse1" />
+  <COLOR Register="0" Spot="0" CMYK="#7800ee11" NAME="Chartreuse2" />
+  <COLOR Register="0" Spot="0" CMYK="#6700cd32" NAME="Chartreuse3" />
+  <COLOR Register="0" Spot="0" CMYK="#46008b74" NAME="Chartreuse4" />
+  <COLOR Register="0" Spot="0" CMYK="#0069b42d" NAME="Chocolate" />
+  <COLOR Register="0" Spot="0" CMYK="#0080db00" NAME="Chocolate1" />
+  <COLOR Register="0" Spot="0" CMYK="#0078cd11" NAME="Chocolate2" />
+  <COLOR Register="0" Spot="0" CMYK="#0067b032" NAME="Chocolate3" />
+  <COLOR Register="0" Spot="0" CMYK="#00467874" NAME="Chocolate4" />
+  <COLOR Register="0" Spot="0" CMYK="#0080af00" NAME="Coral" />
+  <COLOR Register="0" Spot="0" CMYK="#008da900" NAME="Coral1" />
+  <COLOR Register="0" Spot="0" CMYK="#00849e11" NAME="Coral2" />
+  <COLOR Register="0" Spot="0" CMYK="#00728832" NAME="Coral3" />
+  <COLOR Register="0" Spot="0" CMYK="#004d5c74" NAME="Coral4" />
+  <COLOR Register="0" Spot="0" CMYK="#89580012" NAME="CornflowerBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#00072300" NAME="Cornsilk" />
+  <COLOR Register="0" Spot="0" CMYK="#00072300" NAME="Cornsilk1" />
+  <COLOR Register="0" Spot="0" CMYK="#00062111" NAME="Cornsilk2" />
+  <COLOR Register="0" Spot="0" CMYK="#00051c32" NAME="Cornsilk3" />
+  <COLOR Register="0" Spot="0" CMYK="#00031374" NAME="Cornsilk4" />
+  <COLOR Register="0" Spot="0" CMYK="#ff000000" NAME="Cyan" />
+  <COLOR Register="0" Spot="0" CMYK="#ff000000" NAME="Cyan1" />
+  <COLOR Register="0" Spot="0" CMYK="#ee000011" NAME="Cyan2" />
+  <COLOR Register="0" Spot="0" CMYK="#cd000032" NAME="Cyan3" />
+  <COLOR Register="0" Spot="0" CMYK="#8b000074" NAME="Cyan4" />
+  <COLOR Register="0" Spot="0" CMYK="#8b8b0074" NAME="DarkBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#8b000074" NAME="DarkCyan" />
+  <COLOR Register="0" Spot="0" CMYK="#0032ad47" NAME="DarkGoldenrod" />
+  <COLOR Register="0" Spot="0" CMYK="#0046f000" NAME="DarkGoldenrod1" />
+  <COLOR Register="0" Spot="0" CMYK="#0041e011" NAME="DarkGoldenrod2" />
+  <COLOR Register="0" Spot="0" CMYK="#0038c132" NAME="DarkGoldenrod3" />
+  <COLOR Register="0" Spot="0" CMYK="#00268374" NAME="DarkGoldenrod4" />
+  <COLOR Register="0" Spot="0" CMYK="#6400649b" NAME="DarkGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#00000056" NAME="DarkGrey" />
+  <COLOR Register="0" Spot="0" CMYK="#00065242" NAME="DarkKhaki" />
+  <COLOR Register="0" Spot="0" CMYK="#008b0074" NAME="DarkMagenta" />
+  <COLOR Register="0" Spot="0" CMYK="#16003c94" NAME="DarkOliveGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#35008f00" NAME="DarkOliveGreen1" />
+  <COLOR Register="0" Spot="0" CMYK="#32008611" NAME="DarkOliveGreen2" />
+  <COLOR Register="0" Spot="0" CMYK="#2b007332" NAME="DarkOliveGreen3" />
+  <COLOR Register="0" Spot="0" CMYK="#1d004e74" NAME="DarkOliveGreen4" />
+  <COLOR Register="0" Spot="0" CMYK="#0073ff00" NAME="DarkOrange" />
+  <COLOR Register="0" Spot="0" CMYK="#0080ff00" NAME="DarkOrange1" />
+  <COLOR Register="0" Spot="0" CMYK="#0078ee11" NAME="DarkOrange2" />
+  <COLOR Register="0" Spot="0" CMYK="#0067cd32" NAME="DarkOrange3" />
+  <COLOR Register="0" Spot="0" CMYK="#00468b74" NAME="DarkOrange4" />
+  <COLOR Register="0" Spot="0" CMYK="#339a0033" NAME="DarkOrchid" />
+  <COLOR Register="0" Spot="0" CMYK="#40c10000" NAME="DarkOrchid1" />
+  <COLOR Register="0" Spot="0" CMYK="#3cb40011" NAME="DarkOrchid2" />
+  <COLOR Register="0" Spot="0" CMYK="#339b0032" NAME="DarkOrchid3" />
+  <COLOR Register="0" Spot="0" CMYK="#23690074" NAME="DarkOrchid4" />
+  <COLOR Register="0" Spot="0" CMYK="#008b8b74" NAME="DarkRed" />
+  <COLOR Register="0" Spot="0" CMYK="#00536f16" NAME="DarkSalmon" />
+  <COLOR Register="0" Spot="0" CMYK="#2d002d43" NAME="DarkSeaGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#3e003e00" NAME="DarkSeaGreen1" />
+  <COLOR Register="0" Spot="0" CMYK="#3a003a11" NAME="DarkSeaGreen2" />
+  <COLOR Register="0" Spot="0" CMYK="#32003232" NAME="DarkSeaGreen3" />
+  <COLOR Register="0" Spot="0" CMYK="#22002274" NAME="DarkSeaGreen4" />
+  <COLOR Register="0" Spot="0" CMYK="#434e0074" NAME="DarkSlateBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#200000b0" NAME="DarkSlateGrey" />
+  <COLOR Register="0" Spot="0" CMYK="#68000000" NAME="DarkSlateGrey1" />
+  <COLOR Register="0" Spot="0" CMYK="#61000011" NAME="DarkSlateGrey2" />
+  <COLOR Register="0" Spot="0" CMYK="#54000032" NAME="DarkSlateGrey3" />
+  <COLOR Register="0" Spot="0" CMYK="#39000074" NAME="DarkSlateGrey4" />
+  <COLOR Register="0" Spot="0" CMYK="#d103002e" NAME="DarkTurquoise" />
+  <COLOR Register="0" Spot="0" CMYK="#3fd3002c" NAME="DarkViolet" />
+  <COLOR Register="0" Spot="0" CMYK="#00eb6c00" NAME="DeepPink" />
+  <COLOR Register="0" Spot="0" CMYK="#00eb6c00" NAME="DeepPink1" />
+  <COLOR Register="0" Spot="0" CMYK="#00dc6511" NAME="DeepPink2" />
+  <COLOR Register="0" Spot="0" CMYK="#00bd5732" NAME="DeepPink3" />
+  <COLOR Register="0" Spot="0" CMYK="#00813b74" NAME="DeepPink4" />
+  <COLOR Register="0" Spot="0" CMYK="#ff400000" NAME="DeepSkyBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#ff400000" NAME="DeepSkyBlue1" />
+  <COLOR Register="0" Spot="0" CMYK="#ee3c0011" NAME="DeepSkyBlue2" />
+  <COLOR Register="0" Spot="0" CMYK="#cd330032" NAME="DeepSkyBlue3" />
+  <COLOR Register="0" Spot="0" CMYK="#8b230074" NAME="DeepSkyBlue4" />
+  <COLOR Register="0" Spot="0" CMYK="#00000096" NAME="DimGrey" />
+  <COLOR Register="0" Spot="0" CMYK="#e16f0000" NAME="DodgerBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#e16f0000" NAME="DodgerBlue1" />
+  <COLOR Register="0" Spot="0" CMYK="#d2680011" NAME="DodgerBlue2" />
+  <COLOR Register="0" Spot="0" CMYK="#b5590032" NAME="DodgerBlue3" />
+  <COLOR Register="0" Spot="0" CMYK="#7b3d0074" NAME="DodgerBlue4" />
+  <COLOR Register="0" Spot="0" CMYK="#0090904d" NAME="Firebrick" />
+  <COLOR Register="0" Spot="0" CMYK="#00cfcf00" NAME="Firebrick1" />
+  <COLOR Register="0" Spot="0" CMYK="#00c2c211" NAME="Firebrick2" />
+  <COLOR Register="0" Spot="0" CMYK="#00a7a732" NAME="Firebrick3" />
+  <COLOR Register="0" Spot="0" CMYK="#00717174" NAME="Firebrick4" />
+  <COLOR Register="0" Spot="0" CMYK="#00050f00" NAME="FloralWhite" />
+  <COLOR Register="0" Spot="0" CMYK="#69006974" NAME="ForestGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#00000023" NAME="Gainsboro" />
+  <COLOR Register="0" Spot="0" CMYK="#07070000" NAME="GhostWhite" />
+  <COLOR Register="0" Spot="0" CMYK="#0028ff00" NAME="Gold" />
+  <COLOR Register="0" Spot="0" CMYK="#0028ff00" NAME="Gold1" />
+  <COLOR Register="0" Spot="0" CMYK="#0025ee11" NAME="Gold2" />
+  <COLOR Register="0" Spot="0" CMYK="#0020cd32" NAME="Gold3" />
+  <COLOR Register="0" Spot="0" CMYK="#00168b74" NAME="Gold4" />
+  <COLOR Register="0" Spot="0" CMYK="#0035ba25" NAME="Goldenrod" />
+  <COLOR Register="0" Spot="0" CMYK="#003eda00" NAME="Goldenrod1" />
+  <COLOR Register="0" Spot="0" CMYK="#003acc11" NAME="Goldenrod2" />
+  <COLOR Register="0" Spot="0" CMYK="#0032b032" NAME="Goldenrod3" />
+  <COLOR Register="0" Spot="0" CMYK="#00227774" NAME="Goldenrod4" />
+  <COLOR Register="0" Spot="0" CMYK="#ff00ff00" NAME="Green" />
+  <COLOR Register="0" Spot="0" CMYK="#ff00ff00" NAME="Green1" />
+  <COLOR Register="0" Spot="0" CMYK="#ee00ee11" NAME="Green2" />
+  <COLOR Register="0" Spot="0" CMYK="#cd00cd32" NAME="Green3" />
+  <COLOR Register="0" Spot="0" CMYK="#8b008b74" NAME="Green4" />
+  <COLOR Register="0" Spot="0" CMYK="#5200d000" NAME="GreenYellow" />
+  <COLOR Register="0" Spot="0" CMYK="#00000041" NAME="Grey" />
+  <COLOR Register="0" Spot="0" CMYK="#000000ff" NAME="Grey0" />
+  <COLOR Register="0" Spot="0" CMYK="#000000fc" NAME="Grey1" />
+  <COLOR Register="0" Spot="0" CMYK="#000000e5" NAME="Grey10" />
+  <COLOR Register="0" Spot="0" CMYK="#00000000" NAME="Grey100" />
+  <COLOR Register="0" Spot="0" CMYK="#000000e3" NAME="Grey11" />
+  <COLOR Register="0" Spot="0" CMYK="#000000e0" NAME="Grey12" />
+  <COLOR Register="0" Spot="0" CMYK="#000000de" NAME="Grey13" />
+  <COLOR Register="0" Spot="0" CMYK="#000000db" NAME="Grey14" />
+  <COLOR Register="0" Spot="0" CMYK="#000000d9" NAME="Grey15" />
+  <COLOR Register="0" Spot="0" CMYK="#000000d6" NAME="Grey16" />
+  <COLOR Register="0" Spot="0" CMYK="#000000d4" NAME="Grey17" />
+  <COLOR Register="0" Spot="0" CMYK="#000000d1" NAME="Grey18" />
+  <COLOR Register="0" Spot="0" CMYK="#000000cf" NAME="Grey19" />
+  <COLOR Register="0" Spot="0" CMYK="#000000fa" NAME="Grey2" />
+  <COLOR Register="0" Spot="0" CMYK="#000000cc" NAME="Grey20" />
+  <COLOR Register="0" Spot="0" CMYK="#000000c9" NAME="Grey21" />
+  <COLOR Register="0" Spot="0" CMYK="#000000c7" NAME="Grey22" />
+  <COLOR Register="0" Spot="0" CMYK="#000000c4" NAME="Grey23" />
+  <COLOR Register="0" Spot="0" CMYK="#000000c2" NAME="Grey24" />
+  <COLOR Register="0" Spot="0" CMYK="#000000bf" NAME="Grey25" />
+  <COLOR Register="0" Spot="0" CMYK="#000000bd" NAME="Grey26" />
+  <COLOR Register="0" Spot="0" CMYK="#000000ba" NAME="Grey27" />
+  <COLOR Register="0" Spot="0" CMYK="#000000b8" NAME="Grey28" />
+  <COLOR Register="0" Spot="0" CMYK="#000000b5" NAME="Grey29" />
+  <COLOR Register="0" Spot="0" CMYK="#000000f7" NAME="Grey3" />
+  <COLOR Register="0" Spot="0" CMYK="#000000b2" NAME="Grey30" />
+  <COLOR Register="0" Spot="0" CMYK="#000000b0" NAME="Grey31" />
+  <COLOR Register="0" Spot="0" CMYK="#000000ad" NAME="Grey32" />
+  <COLOR Register="0" Spot="0" CMYK="#000000ab" NAME="Grey33" />
+  <COLOR Register="0" Spot="0" CMYK="#000000a8" NAME="Grey34" />
+  <COLOR Register="0" Spot="0" CMYK="#000000a6" NAME="Grey35" />
+  <COLOR Register="0" Spot="0" CMYK="#000000a3" NAME="Grey36" />
+  <COLOR Register="0" Spot="0" CMYK="#000000a1" NAME="Grey37" />
+  <COLOR Register="0" Spot="0" CMYK="#0000009e" NAME="Grey38" />
+  <COLOR Register="0" Spot="0" CMYK="#0000009c" NAME="Grey39" />
+  <COLOR Register="0" Spot="0" CMYK="#000000f5" NAME="Grey4" />
+  <COLOR Register="0" Spot="0" CMYK="#00000099" NAME="Grey40" />
+  <COLOR Register="0" Spot="0" CMYK="#00000096" NAME="Grey41" />
+  <COLOR Register="0" Spot="0" CMYK="#00000094" NAME="Grey42" />
+  <COLOR Register="0" Spot="0" CMYK="#00000091" NAME="Grey43" />
+  <COLOR Register="0" Spot="0" CMYK="#0000008f" NAME="Grey44" />
+  <COLOR Register="0" Spot="0" CMYK="#0000008c" NAME="Grey45" />
+  <COLOR Register="0" Spot="0" CMYK="#0000008a" NAME="Grey46" />
+  <COLOR Register="0" Spot="0" CMYK="#00000087" NAME="Grey47" />
+  <COLOR Register="0" Spot="0" CMYK="#00000085" NAME="Grey48" />
+  <COLOR Register="0" Spot="0" CMYK="#00000082" NAME="Grey49" />
+  <COLOR Register="0" Spot="0" CMYK="#000000f2" NAME="Grey5" />
+  <COLOR Register="0" Spot="0" CMYK="#00000080" NAME="Grey50" />
+  <COLOR Register="0" Spot="0" CMYK="#0000007d" NAME="Grey51" />
+  <COLOR Register="0" Spot="0" CMYK="#0000007a" NAME="Grey52" />
+  <COLOR Register="0" Spot="0" CMYK="#00000078" NAME="Grey53" />
+  <COLOR Register="0" Spot="0" CMYK="#00000075" NAME="Grey54" />
+  <COLOR Register="0" Spot="0" CMYK="#00000073" NAME="Grey55" />
+  <COLOR Register="0" Spot="0" CMYK="#00000070" NAME="Grey56" />
+  <COLOR Register="0" Spot="0" CMYK="#0000006e" NAME="Grey57" />
+  <COLOR Register="0" Spot="0" CMYK="#0000006b" NAME="Grey58" />
+  <COLOR Register="0" Spot="0" CMYK="#00000069" NAME="Grey59" />
+  <COLOR Register="0" Spot="0" CMYK="#000000f0" NAME="Grey6" />
+  <COLOR Register="0" Spot="0" CMYK="#00000066" NAME="Grey60" />
+  <COLOR Register="0" Spot="0" CMYK="#00000063" NAME="Grey61" />
+  <COLOR Register="0" Spot="0" CMYK="#00000061" NAME="Grey62" />
+  <COLOR Register="0" Spot="0" CMYK="#0000005e" NAME="Grey63" />
+  <COLOR Register="0" Spot="0" CMYK="#0000005c" NAME="Grey64" />
+  <COLOR Register="0" Spot="0" CMYK="#00000059" NAME="Grey65" />
+  <COLOR Register="0" Spot="0" CMYK="#00000057" NAME="Grey66" />
+  <COLOR Register="0" Spot="0" CMYK="#00000054" NAME="Grey67" />
+  <COLOR Register="0" Spot="0" CMYK="#00000052" NAME="Grey68" />
+  <COLOR Register="0" Spot="0" CMYK="#0000004f" NAME="Grey69" />
+  <COLOR Register="0" Spot="0" CMYK="#000000ed" NAME="Grey7" />
+  <COLOR Register="0" Spot="0" CMYK="#0000004c" NAME="Grey70" />
+  <COLOR Register="0" Spot="0" CMYK="#0000004a" NAME="Grey71" />
+  <COLOR Register="0" Spot="0" CMYK="#00000047" NAME="Grey72" />
+  <COLOR Register="0" Spot="0" CMYK="#00000045" NAME="Grey73" />
+  <COLOR Register="0" Spot="0" CMYK="#00000042" NAME="Grey74" />
+  <COLOR Register="0" Spot="0" CMYK="#00000040" NAME="Grey75" />
+  <COLOR Register="0" Spot="0" CMYK="#0000003d" NAME="Grey76" />
+  <COLOR Register="0" Spot="0" CMYK="#0000003b" NAME="Grey77" />
+  <COLOR Register="0" Spot="0" CMYK="#00000038" NAME="Grey78" />
+  <COLOR Register="0" Spot="0" CMYK="#00000036" NAME="Grey79" />
+  <COLOR Register="0" Spot="0" CMYK="#000000eb" NAME="Grey8" />
+  <COLOR Register="0" Spot="0" CMYK="#00000033" NAME="Grey80" />
+  <COLOR Register="0" Spot="0" CMYK="#00000030" NAME="Grey81" />
+  <COLOR Register="0" Spot="0" CMYK="#0000002e" NAME="Grey82" />
+  <COLOR Register="0" Spot="0" CMYK="#0000002b" NAME="Grey83" />
+  <COLOR Register="0" Spot="0" CMYK="#00000029" NAME="Grey84" />
+  <COLOR Register="0" Spot="0" CMYK="#00000026" NAME="Grey85" />
+  <COLOR Register="0" Spot="0" CMYK="#00000024" NAME="Grey86" />
+  <COLOR Register="0" Spot="0" CMYK="#00000021" NAME="Grey87" />
+  <COLOR Register="0" Spot="0" CMYK="#0000001f" NAME="Grey88" />
+  <COLOR Register="0" Spot="0" CMYK="#0000001c" NAME="Grey89" />
+  <COLOR Register="0" Spot="0" CMYK="#000000e8" NAME="Grey9" />
+  <COLOR Register="0" Spot="0" CMYK="#0000001a" NAME="Grey90" />
+  <COLOR Register="0" Spot="0" CMYK="#00000017" NAME="Grey91" />
+  <COLOR Register="0" Spot="0" CMYK="#00000014" NAME="Grey92" />
+  <COLOR Register="0" Spot="0" CMYK="#00000012" NAME="Grey93" />
+  <COLOR Register="0" Spot="0" CMYK="#0000000f" NAME="Grey94" />
+  <COLOR Register="0" Spot="0" CMYK="#0000000d" NAME="Grey95" />
+  <COLOR Register="0" Spot="0" CMYK="#0000000a" NAME="Grey96" />
+  <COLOR Register="0" Spot="0" CMYK="#00000008" NAME="Grey97" />
+  <COLOR Register="0" Spot="0" CMYK="#00000005" NAME="Grey98" />
+  <COLOR Register="0" Spot="0" CMYK="#00000003" NAME="Grey99" />
+  <COLOR Register="0" Spot="0" CMYK="#0f000f00" NAME="Honeydew" />
+  <COLOR Register="0" Spot="0" CMYK="#0f000f00" NAME="Honeydew1" />
+  <COLOR Register="0" Spot="0" CMYK="#0e000e11" NAME="Honeydew2" />
+  <COLOR Register="0" Spot="0" CMYK="#0c000c32" NAME="Honeydew3" />
+  <COLOR Register="0" Spot="0" CMYK="#08000874" NAME="Honeydew4" />
+  <COLOR Register="0" Spot="0" CMYK="#00964b00" NAME="HotPink" />
+  <COLOR Register="0" Spot="0" CMYK="#00914b00" NAME="HotPink1" />
+  <COLOR Register="0" Spot="0" CMYK="#00844711" NAME="HotPink2" />
+  <COLOR Register="0" Spot="0" CMYK="#006d3d32" NAME="HotPink3" />
+  <COLOR Register="0" Spot="0" CMYK="#00512974" NAME="HotPink4" />
+  <COLOR Register="0" Spot="0" CMYK="#00717132" NAME="IndianRed" />
+  <COLOR Register="0" Spot="0" CMYK="#00959500" NAME="IndianRed1" />
+  <COLOR Register="0" Spot="0" CMYK="#008b8b11" NAME="IndianRed2" />
+  <COLOR Register="0" Spot="0" CMYK="#00787832" NAME="IndianRed3" />
+  <COLOR Register="0" Spot="0" CMYK="#00515174" NAME="IndianRed4" />
+  <COLOR Register="0" Spot="0" CMYK="#00000f00" NAME="Ivory" />
+  <COLOR Register="0" Spot="0" CMYK="#00000f00" NAME="Ivory1" />
+  <COLOR Register="0" Spot="0" CMYK="#00000e11" NAME="Ivory2" />
+  <COLOR Register="0" Spot="0" CMYK="#00000c32" NAME="Ivory3" />
+  <COLOR Register="0" Spot="0" CMYK="#00000874" NAME="Ivory4" />
+  <COLOR Register="0" Spot="0" CMYK="#000a640f" NAME="Khaki" />
+  <COLOR Register="0" Spot="0" CMYK="#00097000" NAME="Khaki1" />
+  <COLOR Register="0" Spot="0" CMYK="#00086911" NAME="Khaki2" />
+  <COLOR Register="0" Spot="0" CMYK="#00075a32" NAME="Khaki3" />
+  <COLOR Register="0" Spot="0" CMYK="#00053d74" NAME="Khaki4" />
+  <COLOR Register="0" Spot="0" CMYK="#14140005" NAME="Lavender" />
+  <COLOR Register="0" Spot="0" CMYK="#000f0a00" NAME="LavenderBlush" />
+  <COLOR Register="0" Spot="0" CMYK="#000f0a00" NAME="LavenderBlush1" />
+  <COLOR Register="0" Spot="0" CMYK="#000e0911" NAME="LavenderBlush2" />
+  <COLOR Register="0" Spot="0" CMYK="#000c0832" NAME="LavenderBlush3" />
+  <COLOR Register="0" Spot="0" CMYK="#00080574" NAME="LavenderBlush4" />
+  <COLOR Register="0" Spot="0" CMYK="#8000fc03" NAME="LawnGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#00053200" NAME="LemonChiffon" />
+  <COLOR Register="0" Spot="0" CMYK="#00053200" NAME="LemonChiffon1" />
+  <COLOR Register="0" Spot="0" CMYK="#00052f11" NAME="LemonChiffon2" />
+  <COLOR Register="0" Spot="0" CMYK="#00042832" NAME="LemonChiffon3" />
+  <COLOR Register="0" Spot="0" CMYK="#00021b74" NAME="LemonChiffon4" />
+  <COLOR Register="0" Spot="0" CMYK="#390e0019" NAME="LightBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#40100000" NAME="LightBlue1" />
+  <COLOR Register="0" Spot="0" CMYK="#3c0f0011" NAME="LightBlue2" />
+  <COLOR Register="0" Spot="0" CMYK="#330d0032" NAME="LightBlue3" />
+  <COLOR Register="0" Spot="0" CMYK="#23080074" NAME="LightBlue4" />
+  <COLOR Register="0" Spot="0" CMYK="#0070700f" NAME="LightCoral" />
+  <COLOR Register="0" Spot="0" CMYK="#1f000000" NAME="LightCyan" />
+  <COLOR Register="0" Spot="0" CMYK="#1f000000" NAME="LightCyan1" />
+  <COLOR Register="0" Spot="0" CMYK="#1d000011" NAME="LightCyan2" />
+  <COLOR Register="0" Spot="0" CMYK="#19000032" NAME="LightCyan3" />
+  <COLOR Register="0" Spot="0" CMYK="#11000074" NAME="LightCyan4" />
+  <COLOR Register="0" Spot="0" CMYK="#00116c11" NAME="LightGoldenrod" />
+  <COLOR Register="0" Spot="0" CMYK="#00137400" NAME="LightGoldenrod1" />
+  <COLOR Register="0" Spot="0" CMYK="#00126c11" NAME="LightGoldenrod2" />
+  <COLOR Register="0" Spot="0" CMYK="#000f5d32" NAME="LightGoldenrod3" />
+  <COLOR Register="0" Spot="0" CMYK="#000a3f74" NAME="LightGoldenrod4" />
+  <COLOR Register="0" Spot="0" CMYK="#00002805" NAME="LightGoldenrodYellow" />
+  <COLOR Register="0" Spot="0" CMYK="#5e005e11" NAME="LightGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#0000002c" NAME="LightGrey" />
+  <COLOR Register="0" Spot="0" CMYK="#00493e00" NAME="LightPink" />
+  <COLOR Register="0" Spot="0" CMYK="#00514600" NAME="LightPink1" />
+  <COLOR Register="0" Spot="0" CMYK="#004c4111" NAME="LightPink2" />
+  <COLOR Register="0" Spot="0" CMYK="#00413832" NAME="LightPink3" />
+  <COLOR Register="0" Spot="0" CMYK="#002c2674" NAME="LightPink4" />
+  <COLOR Register="0" Spot="0" CMYK="#005f8500" NAME="LightSalmon" />
+  <COLOR Register="0" Spot="0" CMYK="#005f8500" NAME="LightSalmon1" />
+  <COLOR Register="0" Spot="0" CMYK="#00597c11" NAME="LightSalmon2" />
+  <COLOR Register="0" Spot="0" CMYK="#004c6b32" NAME="LightSalmon3" />
+  <COLOR Register="0" Spot="0" CMYK="#00344974" NAME="LightSalmon4" />
+  <COLOR Register="0" Spot="0" CMYK="#9200084d" NAME="LightSeaGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#732c0005" NAME="LightSkyBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#4f1d0000" NAME="LightSkyBlue1" />
+  <COLOR Register="0" Spot="0" CMYK="#4a1b0011" NAME="LightSkyBlue2" />
+  <COLOR Register="0" Spot="0" CMYK="#40170032" NAME="LightSkyBlue3" />
+  <COLOR Register="0" Spot="0" CMYK="#2b100074" NAME="LightSkyBlue4" />
+  <COLOR Register="0" Spot="0" CMYK="#7b8f0000" NAME="LightSlateBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#22110066" NAME="LightSlateGrey" />
+  <COLOR Register="0" Spot="0" CMYK="#2e1a0021" NAME="LightSteelBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#351e0000" NAME="LightSteelBlue1" />
+  <COLOR Register="0" Spot="0" CMYK="#321c0011" NAME="LightSteelBlue2" />
+  <COLOR Register="0" Spot="0" CMYK="#2b180032" NAME="LightSteelBlue3" />
+  <COLOR Register="0" Spot="0" CMYK="#1d100074" NAME="LightSteelBlue4" />
+  <COLOR Register="0" Spot="0" CMYK="#00001f00" NAME="LightYellow" />
+  <COLOR Register="0" Spot="0" CMYK="#00001f00" NAME="LightYellow1" />
+  <COLOR Register="0" Spot="0" CMYK="#00001d11" NAME="LightYellow2" />
+  <COLOR Register="0" Spot="0" CMYK="#00001932" NAME="LightYellow3" />
+  <COLOR Register="0" Spot="0" CMYK="#00001174" NAME="LightYellow4" />
+  <COLOR Register="0" Spot="0" CMYK="#9b009b32" NAME="LimeGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#000a1405" NAME="Linen" />
+  <COLOR Register="0" Spot="0" CMYK="#00ff0000" NAME="Magenta" />
+  <COLOR Register="0" Spot="0" CMYK="#00ff0000" NAME="Magenta1" />
+  <COLOR Register="0" Spot="0" CMYK="#00ee0011" NAME="Magenta2" />
+  <COLOR Register="0" Spot="0" CMYK="#00cd0032" NAME="Magenta3" />
+  <COLOR Register="0" Spot="0" CMYK="#008b0074" NAME="Magenta4" />
+  <COLOR Register="0" Spot="0" CMYK="#0080504f" NAME="Maroon" />
+  <COLOR Register="0" Spot="0" CMYK="#00cb4c00" NAME="Maroon1" />
+  <COLOR Register="0" Spot="0" CMYK="#00be4711" NAME="Maroon2" />
+  <COLOR Register="0" Spot="0" CMYK="#00a43d32" NAME="Maroon3" />
+  <COLOR Register="0" Spot="0" CMYK="#006f2974" NAME="Maroon4" />
+  <COLOR Register="0" Spot="0" CMYK="#67002332" NAME="MediumAquamarine" />
+  <COLOR Register="0" Spot="0" CMYK="#cdcd0032" NAME="MediumBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#197e002c" NAME="MediumOrchid" />
+  <COLOR Register="0" Spot="0" CMYK="#1f990000" NAME="MediumOrchid1" />
+  <COLOR Register="0" Spot="0" CMYK="#1d8f0011" NAME="MediumOrchid2" />
+  <COLOR Register="0" Spot="0" CMYK="#197b0032" NAME="MediumOrchid3" />
+  <COLOR Register="0" Spot="0" CMYK="#11540074" NAME="MediumOrchid4" />
+  <COLOR Register="0" Spot="0" CMYK="#486b0024" NAME="MediumPurple" />
+  <COLOR Register="0" Spot="0" CMYK="#547d0000" NAME="MediumPurple1" />
+  <COLOR Register="0" Spot="0" CMYK="#4f750011" NAME="MediumPurple2" />
+  <COLOR Register="0" Spot="0" CMYK="#44650032" NAME="MediumPurple3" />
+  <COLOR Register="0" Spot="0" CMYK="#2e440074" NAME="MediumPurple4" />
+  <COLOR Register="0" Spot="0" CMYK="#7700424c" NAME="MediumSeaGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#73860011" NAME="MediumSlateBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#fa006005" NAME="MediumSpringGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#8900052e" NAME="MediumTurquoise" />
+  <COLOR Register="0" Spot="0" CMYK="#00b24238" NAME="MediumVioletRed" />
+  <COLOR Register="0" Spot="0" CMYK="#5757008f" NAME="MidnightBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#0a000500" NAME="MintCream" />
+  <COLOR Register="0" Spot="0" CMYK="#001b1e00" NAME="MistyRose" />
+  <COLOR Register="0" Spot="0" CMYK="#001b1e00" NAME="MistyRose1" />
+  <COLOR Register="0" Spot="0" CMYK="#00191c11" NAME="MistyRose2" />
+  <COLOR Register="0" Spot="0" CMYK="#00161832" NAME="MistyRose3" />
+  <COLOR Register="0" Spot="0" CMYK="#000e1074" NAME="MistyRose4" />
+  <COLOR Register="0" Spot="0" CMYK="#001b4a00" NAME="Moccasin" />
+  <COLOR Register="0" Spot="0" CMYK="#00215200" NAME="NavajoWhite" />
+  <COLOR Register="0" Spot="0" CMYK="#00215200" NAME="NavajoWhite1" />
+  <COLOR Register="0" Spot="0" CMYK="#001f4d11" NAME="NavajoWhite2" />
+  <COLOR Register="0" Spot="0" CMYK="#001a4232" NAME="NavajoWhite3" />
+  <COLOR Register="0" Spot="0" CMYK="#00122d74" NAME="NavajoWhite4" />
+  <COLOR Register="0" Spot="0" CMYK="#8080007f" NAME="NavyBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#00081702" NAME="OldLace" />
+  <COLOR Register="0" Spot="0" CMYK="#23006b71" NAME="OliveDrab" />
+  <COLOR Register="0" Spot="0" CMYK="#3f00c100" NAME="OliveDrab1" />
+  <COLOR Register="0" Spot="0" CMYK="#3b00b411" NAME="OliveDrab2" />
+  <COLOR Register="0" Spot="0" CMYK="#33009b32" NAME="OliveDrab3" />
+  <COLOR Register="0" Spot="0" CMYK="#22006974" NAME="OliveDrab4" />
+  <COLOR Register="0" Spot="0" CMYK="#005aff00" NAME="Orange" />
+  <COLOR Register="0" Spot="0" CMYK="#005aff00" NAME="Orange1" />
+  <COLOR Register="0" Spot="0" CMYK="#0054ee11" NAME="Orange2" />
+  <COLOR Register="0" Spot="0" CMYK="#0048cd32" NAME="Orange3" />
+  <COLOR Register="0" Spot="0" CMYK="#00318b74" NAME="Orange4" />
+  <COLOR Register="0" Spot="0" CMYK="#00baff00" NAME="OrangeRed" />
+  <COLOR Register="0" Spot="0" CMYK="#00baff00" NAME="OrangeRed1" />
+  <COLOR Register="0" Spot="0" CMYK="#00aeee11" NAME="OrangeRed2" />
+  <COLOR Register="0" Spot="0" CMYK="#0096cd32" NAME="OrangeRed3" />
+  <COLOR Register="0" Spot="0" CMYK="#00668b74" NAME="OrangeRed4" />
+  <COLOR Register="0" Spot="0" CMYK="#006a0425" NAME="Orchid" />
+  <COLOR Register="0" Spot="0" CMYK="#007c0500" NAME="Orchid1" />
+  <COLOR Register="0" Spot="0" CMYK="#00740511" NAME="Orchid2" />
+  <COLOR Register="0" Spot="0" CMYK="#00640432" NAME="Orchid3" />
+  <COLOR Register="0" Spot="0" CMYK="#00440274" NAME="Orchid4" />
+  <COLOR Register="0" Spot="0" CMYK="#00064411" NAME="PaleGoldenrod" />
+  <COLOR Register="0" Spot="0" CMYK="#63006304" NAME="PaleGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#65006500" NAME="PaleGreen1" />
+  <COLOR Register="0" Spot="0" CMYK="#5e005e11" NAME="PaleGreen2" />
+  <COLOR Register="0" Spot="0" CMYK="#51005132" NAME="PaleGreen3" />
+  <COLOR Register="0" Spot="0" CMYK="#37003774" NAME="PaleGreen4" />
+  <COLOR Register="0" Spot="0" CMYK="#3f000011" NAME="PaleTurquoise" />
+  <COLOR Register="0" Spot="0" CMYK="#44000000" NAME="PaleTurquoise1" />
+  <COLOR Register="0" Spot="0" CMYK="#40000011" NAME="PaleTurquoise2" />
+  <COLOR Register="0" Spot="0" CMYK="#37000032" NAME="PaleTurquoise3" />
+  <COLOR Register="0" Spot="0" CMYK="#25000074" NAME="PaleTurquoise4" />
+  <COLOR Register="0" Spot="0" CMYK="#006b4824" NAME="PaleVioletRed" />
+  <COLOR Register="0" Spot="0" CMYK="#007d5400" NAME="PaleVioletRed1" />
+  <COLOR Register="0" Spot="0" CMYK="#00754f11" NAME="PaleVioletRed2" />
+  <COLOR Register="0" Spot="0" CMYK="#00654432" NAME="PaleVioletRed3" />
+  <COLOR Register="0" Spot="0" CMYK="#00442e74" NAME="PaleVioletRed4" />
+  <COLOR Register="0" Spot="0" CMYK="#00102a00" NAME="PapayaWhip" />
+  <COLOR Register="0" Spot="0" CMYK="#00254600" NAME="PeachPuff" />
+  <COLOR Register="0" Spot="0" CMYK="#00254600" NAME="PeachPuff1" />
+  <COLOR Register="0" Spot="0" CMYK="#00234111" NAME="PeachPuff2" />
+  <COLOR Register="0" Spot="0" CMYK="#001e3832" NAME="PeachPuff3" />
+  <COLOR Register="0" Spot="0" CMYK="#00142674" NAME="PeachPuff4" />
+  <COLOR Register="0" Spot="0" CMYK="#00488e32" NAME="Peru" />
+  <COLOR Register="0" Spot="0" CMYK="#003f3400" NAME="Pink" />
+  <COLOR Register="0" Spot="0" CMYK="#004a3a00" NAME="Pink1" />
+  <COLOR Register="0" Spot="0" CMYK="#00453611" NAME="Pink2" />
+  <COLOR Register="0" Spot="0" CMYK="#003c2f32" NAME="Pink3" />
+  <COLOR Register="0" Spot="0" CMYK="#00281f74" NAME="Pink4" />
+  <COLOR Register="0" Spot="0" CMYK="#003d0022" NAME="Plum" />
+  <COLOR Register="0" Spot="0" CMYK="#00440000" NAME="Plum1" />
+  <COLOR Register="0" Spot="0" CMYK="#00400011" NAME="Plum2" />
+  <COLOR Register="0" Spot="0" CMYK="#00370032" NAME="Plum3" />
+  <COLOR Register="0" Spot="0" CMYK="#00250074" NAME="Plum4" />
+  <COLOR Register="0" Spot="0" CMYK="#36060019" NAME="PowderBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#50d0000f" NAME="Purple" />
+  <COLOR Register="0" Spot="0" CMYK="#64cf0000" NAME="Purple1" />
+  <COLOR Register="0" Spot="0" CMYK="#5dc20011" NAME="Purple2" />
+  <COLOR Register="0" Spot="0" CMYK="#50a70032" NAME="Purple3" />
+  <COLOR Register="0" Spot="0" CMYK="#36710074" NAME="Purple4" />
+  <COLOR Register="0" Spot="0" CMYK="#00ffff00" NAME="Red" />
+  <COLOR Register="0" Spot="0" CMYK="#00ffff00" NAME="Red1" />
+  <COLOR Register="0" Spot="0" CMYK="#00eeee11" NAME="Red2" />
+  <COLOR Register="0" Spot="0" CMYK="#00cdcd32" NAME="Red3" />
+  <COLOR Register="0" Spot="0" CMYK="#008b8b74" NAME="Red4" />
+  <COLOR Register="0" Spot="0" CMYK="#002d2d43" NAME="RosyBrown" />
+  <COLOR Register="0" Spot="0" CMYK="#003e3e00" NAME="RosyBrown1" />
+  <COLOR Register="0" Spot="0" CMYK="#003a3a11" NAME="RosyBrown2" />
+  <COLOR Register="0" Spot="0" CMYK="#00323232" NAME="RosyBrown3" />
+  <COLOR Register="0" Spot="0" CMYK="#00222274" NAME="RosyBrown4" />
+  <COLOR Register="0" Spot="0" CMYK="#a078001e" NAME="RoyalBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#b7890000" NAME="RoyalBlue1" />
+  <COLOR Register="0" Spot="0" CMYK="#ab800011" NAME="RoyalBlue2" />
+  <COLOR Register="0" Spot="0" CMYK="#936e0032" NAME="RoyalBlue3" />
+  <COLOR Register="0" Spot="0" CMYK="#644b0074" NAME="RoyalBlue4" />
+  <COLOR Register="0" Spot="0" CMYK="#00467874" NAME="SaddleBrown" />
+  <COLOR Register="0" Spot="0" CMYK="#007a8805" NAME="Salmon" />
+  <COLOR Register="0" Spot="0" CMYK="#00739600" NAME="Salmon1" />
+  <COLOR Register="0" Spot="0" CMYK="#006c8c11" NAME="Salmon2" />
+  <COLOR Register="0" Spot="0" CMYK="#005d7932" NAME="Salmon3" />
+  <COLOR Register="0" Spot="0" CMYK="#003f5274" NAME="Salmon4" />
+  <COLOR Register="0" Spot="0" CMYK="#0050940b" NAME="SandyBrown" />
+  <COLOR Register="0" Spot="0" CMYK="#5d003474" NAME="SeaGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#ab006000" NAME="SeaGreen1" />
+  <COLOR Register="0" Spot="0" CMYK="#a0005a11" NAME="SeaGreen2" />
+  <COLOR Register="0" Spot="0" CMYK="#8a004d32" NAME="SeaGreen3" />
+  <COLOR Register="0" Spot="0" CMYK="#5d003474" NAME="SeaGreen4" />
+  <COLOR Register="0" Spot="0" CMYK="#000a1100" NAME="Seashell" />
+  <COLOR Register="0" Spot="0" CMYK="#000a1100" NAME="Seashell1" />
+  <COLOR Register="0" Spot="0" CMYK="#00091011" NAME="Seashell2" />
+  <COLOR Register="0" Spot="0" CMYK="#00080e32" NAME="Seashell3" />
+  <COLOR Register="0" Spot="0" CMYK="#00050974" NAME="Seashell4" />
+  <COLOR Register="0" Spot="0" CMYK="#004e735f" NAME="Sienna" />
+  <COLOR Register="0" Spot="0" CMYK="#007db800" NAME="Sienna1" />
+  <COLOR Register="0" Spot="0" CMYK="#0075ac11" NAME="Sienna2" />
+  <COLOR Register="0" Spot="0" CMYK="#00659432" NAME="Sienna3" />
+  <COLOR Register="0" Spot="0" CMYK="#00446574" NAME="Sienna4" />
+  <COLOR Register="0" Spot="0" CMYK="#641d0014" NAME="SkyBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#78310000" NAME="SkyBlue1" />
+  <COLOR Register="0" Spot="0" CMYK="#702e0011" NAME="SkyBlue2" />
+  <COLOR Register="0" Spot="0" CMYK="#61270032" NAME="SkyBlue3" />
+  <COLOR Register="0" Spot="0" CMYK="#411b0074" NAME="SkyBlue4" />
+  <COLOR Register="0" Spot="0" CMYK="#63730032" NAME="SlateBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#7c900000" NAME="SlateBlue1" />
+  <COLOR Register="0" Spot="0" CMYK="#74870011" NAME="SlateBlue2" />
+  <COLOR Register="0" Spot="0" CMYK="#64740032" NAME="SlateBlue3" />
+  <COLOR Register="0" Spot="0" CMYK="#444f0074" NAME="SlateBlue4" />
+  <COLOR Register="0" Spot="0" CMYK="#2010006f" NAME="SlateGrey" />
+  <COLOR Register="0" Spot="0" CMYK="#391d0000" NAME="SlateGrey1" />
+  <COLOR Register="0" Spot="0" CMYK="#351b0011" NAME="SlateGrey2" />
+  <COLOR Register="0" Spot="0" CMYK="#2e170032" NAME="SlateGrey3" />
+  <COLOR Register="0" Spot="0" CMYK="#1f100074" NAME="SlateGrey4" />
+  <COLOR Register="0" Spot="0" CMYK="#00050500" NAME="Snow" />
+  <COLOR Register="0" Spot="0" CMYK="#00050500" NAME="Snow1" />
+  <COLOR Register="0" Spot="0" CMYK="#00050511" NAME="Snow2" />
+  <COLOR Register="0" Spot="0" CMYK="#00040432" NAME="Snow3" />
+  <COLOR Register="0" Spot="0" CMYK="#00020274" NAME="Snow4" />
+  <COLOR Register="0" Spot="0" CMYK="#ff008000" NAME="SpringGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#ff008000" NAME="SpringGreen1" />
+  <COLOR Register="0" Spot="0" CMYK="#ee007811" NAME="SpringGreen2" />
+  <COLOR Register="0" Spot="0" CMYK="#cd006732" NAME="SpringGreen3" />
+  <COLOR Register="0" Spot="0" CMYK="#8b004674" NAME="SpringGreen4" />
+  <COLOR Register="0" Spot="0" CMYK="#6e32004b" NAME="SteelBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#9c470000" NAME="SteelBlue1" />
+  <COLOR Register="0" Spot="0" CMYK="#92420011" NAME="SteelBlue2" />
+  <COLOR Register="0" Spot="0" CMYK="#7e390032" NAME="SteelBlue3" />
+  <COLOR Register="0" Spot="0" CMYK="#55270074" NAME="SteelBlue4" />
+  <COLOR Register="0" Spot="0" CMYK="#001e462d" NAME="Tan" />
+  <COLOR Register="0" Spot="0" CMYK="#005ab000" NAME="Tan1" />
+  <COLOR Register="0" Spot="0" CMYK="#0054a511" NAME="Tan2" />
+  <COLOR Register="0" Spot="0" CMYK="#00488e32" NAME="Tan3" />
+  <COLOR Register="0" Spot="0" CMYK="#00316074" NAME="Tan4" />
+  <COLOR Register="0" Spot="0" CMYK="#00190027" NAME="Thistle" />
+  <COLOR Register="0" Spot="0" CMYK="#001e0000" NAME="Thistle1" />
+  <COLOR Register="0" Spot="0" CMYK="#001c0011" NAME="Thistle2" />
+  <COLOR Register="0" Spot="0" CMYK="#00180032" NAME="Thistle3" />
+  <COLOR Register="0" Spot="0" CMYK="#00100074" NAME="Thistle4" />
+  <COLOR Register="0" Spot="0" CMYK="#009cb800" NAME="Tomato" />
+  <COLOR Register="0" Spot="0" CMYK="#009cb800" NAME="Tomato1" />
+  <COLOR Register="0" Spot="0" CMYK="#0092ac11" NAME="Tomato2" />
+  <COLOR Register="0" Spot="0" CMYK="#007e9432" NAME="Tomato3" />
+  <COLOR Register="0" Spot="0" CMYK="#00556574" NAME="Tomato4" />
+  <COLOR Register="0" Spot="0" CMYK="#a000101f" NAME="Turquoise" />
+  <COLOR Register="0" Spot="0" CMYK="#ff0a0000" NAME="Turquoise1" />
+  <COLOR Register="0" Spot="0" CMYK="#ee090011" NAME="Turquoise2" />
+  <COLOR Register="0" Spot="0" CMYK="#cd080032" NAME="Turquoise3" />
+  <COLOR Register="0" Spot="0" CMYK="#8b050074" NAME="Turquoise4" />
+  <COLOR Register="0" Spot="0" CMYK="#006c0011" NAME="Violet" />
+  <COLOR Register="0" Spot="0" CMYK="#00b0402f" NAME="VioletRed" />
+  <COLOR Register="0" Spot="0" CMYK="#00c16900" NAME="VioletRed1" />
+  <COLOR Register="0" Spot="0" CMYK="#00b46211" NAME="VioletRed2" />
+  <COLOR Register="0" Spot="0" CMYK="#009b5532" NAME="VioletRed3" />
+  <COLOR Register="0" Spot="0" CMYK="#00693974" NAME="VioletRed4" />
+  <COLOR Register="0" Spot="0" CMYK="#0017420a" NAME="Wheat" />
+  <COLOR Register="0" Spot="0" CMYK="#00184500" NAME="Wheat1" />
+  <COLOR Register="0" Spot="0" CMYK="#00164011" NAME="Wheat2" />
+  <COLOR Register="0" Spot="0" CMYK="#00133732" NAME="Wheat3" />
+  <COLOR Register="0" Spot="0" CMYK="#000d2574" NAME="Wheat4" />
+  <COLOR Register="0" Spot="0" CMYK="#00000000" NAME="White" />
+  <COLOR Register="0" Spot="0" CMYK="#0000000a" NAME="WhiteSmoke" />
+  <COLOR Register="0" Spot="0" CMYK="#0000ff00" NAME="Yellow" />
+  <COLOR Register="0" Spot="0" CMYK="#0000ff00" NAME="Yellow1" />
+  <COLOR Register="0" Spot="0" CMYK="#0000ee11" NAME="Yellow2" />
+  <COLOR Register="0" Spot="0" CMYK="#0000cd32" NAME="Yellow3" />
+  <COLOR Register="0" Spot="0" CMYK="#00008b74" NAME="Yellow4" />
+  <COLOR Register="0" Spot="0" CMYK="#33009b32" NAME="YellowGreen" />
+  <LAYERS DRUCKEN="1" NUMMER="0" EDIT="1" NAME="Hintergrund" SICHTBAR="1" LEVEL="0" />
+  <PDF displayThumbs="0" ImagePr="0" fitWindow="0" displayBookmarks="0" BTop="9" UseProfiles="0" BLeft="9" PrintP="" RecalcPic="0" UseSpotColors="1" ImageP="" SolidP="" PicRes="300" Thumbnails="0" hideToolBar="0" CMethod="0" displayLayers="0" doMultiFile="0" UseLayers="0" Encrypt="0" BRight="9" Binding="0" Articles="0" InfoString="" RGBMode="1" Grayscale="0" PresentMode="0" openAction="" displayFullscreen="0" Permissions="-4" Intent="1" Compress="1" hideMenuBar="0" Version="14" Resolution="300" Bookmarks="0" UseProfiles2="0" RotateDeg="0" Clip="0" MirrorV="0" Quality="0" PageLayout="0" UseLpi="0" PassUser="" BBottom="9" Intent2="1" MirrorH="0" PassOwner="" >
+   <LPI Angle="45" Frequency="75" SpotFunction="2" Color="Black" />
+   <LPI Angle="105" Frequency="75" SpotFunction="2" Color="Cyan" />
+   <LPI Angle="75" Frequency="75" SpotFunction="2" Color="Magenta" />
+   <LPI Angle="90" Frequency="75" SpotFunction="2" Color="Yellow" />
+  </PDF>
+  <DocItemAttributes/>
+  <TablesOfContents/>
+  <Sections>
+   <Section Active="1" Number="0" From="0" Type="Type_1_2_3" To="0" Name="0" Start="1" Reversed="0" />
+  </Sections>
+  <PageSets>
+   <Set Columns="1" GapBelow="40" Rows="1" FirstPage="0" GapHorizontal="0" Name="Single Page" GapVertical="0" />
+   <Set Columns="2" GapBelow="40" Rows="1" FirstPage="1" GapHorizontal="0" Name="Double Sided" GapVertical="0" >
+    <PageNames Name="Left Page" />
+    <PageNames Name="Right Page" />
+   </Set>
+   <Set Columns="3" GapBelow="40" Rows="1" FirstPage="0" GapHorizontal="0" Name="3-Fold" GapVertical="0" >
+    <PageNames Name="Left Page" />
+    <PageNames Name="Middle" />
+    <PageNames Name="Right Page" />
+   </Set>
+   <Set Columns="4" GapBelow="40" Rows="1" FirstPage="0" GapHorizontal="0" Name="4-Fold" GapVertical="0" >
+    <PageNames Name="Left Page" />
+    <PageNames Name="Middle Left" />
+    <PageNames Name="Middle Right" />
+    <PageNames Name="Right Page" />
+   </Set>
+  </PageSets>
+  <MASTERPAGE Size="A6" NUM="0" BORDERTOP="9" NAM="Normal" LEFT="0" BORDERBOTTOM="9" Orientation="1" BORDERRIGHT="9" NumVGuides="0" PAGEHEIGHT="297.64" PAGEWIDTH="419.53" PAGEYPOS="20" HorizontalGuides="" MNAM="" PAGEXPOS="100" NumHGuides="0" VerticalGuides="" BORDERLEFT="9" />
+  <PAGE Size="A5" NUM="0" BORDERTOP="9" NAM="" LEFT="0" BORDERBOTTOM="9" Orientation="1" BORDERRIGHT="9" NumVGuides="0" PAGEHEIGHT="419.53" PAGEWIDTH="595.28" PAGEYPOS="20" HorizontalGuides="" MNAM="Normal" PAGEXPOS="100" NumHGuides="0" VerticalGuides="" BORDERLEFT="9" />
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="-1.79417e+308" gWidth="-1.79417e+308" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="196" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Beige" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="0" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="372" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 196 0 196 0 196 0 196 0 196 372 196 372 196 372 196 372 0 372 0 372 0 372 0 372 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="-1.79417e+308" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="-1.79417e+308" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="29" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="290" NUMCO="16" POCOOR="0 0 0 0 196 0 196 0 196 0 196 0 196 372 196 372 196 372 196 372 0 372 0 372 0 372 0 372 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="3.63841e-304" gWidth="3.63841e-304" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="116" ImageRes="1" GROUPS="57 58 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Bisque3" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="2" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1.9963" HEIGHT="122" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="1" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 116 0 116 0 116 0 116 0 116 122 116 122 116 122 116 122 0 122 0 122 0 122 0 122 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="3.63841e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="3.63841e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="214" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="325" NUMCO="16" POCOOR="0 0 0 0 116 0 116 0 116 0 116 0 116 122 116 122 116 122 116 122 0 122 0 122 0 122 0 122 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="1" gHeight="7.29112e-304" gWidth="7.29112e-304" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="76.85" ImageRes="1" GROUPS="57 58 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="2" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1.9963" HEIGHT="25.0741" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 76.85 0 76.85 0 76.85 0 76.85 0 76.85 25.0741 76.85 25.0741 76.85 25.0741 76.85 25.0741 0 25.0741 0 25.0741 0 25.0741 0 25.0741 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="7.29112e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="7.29112e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="220.926" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="344.65" NUMCO="16" POCOOR="0 0 0 0 76.85 0 76.85 0 76.85 0 76.85 0 76.85 25.0741 76.85 25.0741 76.85 25.0741 76.85 25.0741 0 25.0741 0 25.0741 0 25.0741 0 25.0741 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT CAB="1" CCOLOR="Black" CBASE="0" CSTW="-0.1" CSIZE="12" CULP="-0.1" CH="Storage" CSHADE2="100" CKERN="0" CSCALE="100" CSHADE="100" COUT="1" CSCALEV="100" CSTP="-0.1" CULW="-0.1" CSTYLE="0" CFONT="Bitstream Charter Bold Italic" CSHX="5" CSHY="-5" CSTROKE="Black" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="-1.79417e+308" gWidth="-1.79417e+308" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="144" ImageRes="1" GROUPS="1 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Aquamarine3" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="1" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="42" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="144 21 144 32.5984 72 42 111.766 42 72 42 32.2355 42 0 21 0 32.5984 0 21 0 9.40202 72 0 32.2355 0 72 0 111.766 0 144 21 144 9.40202 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="-1.79417e+308" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="-1.79417e+308" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="94" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="502" NUMCO="16" POCOOR="144 21 144 32.5984 72 42 111.766 42 72 42 32.2355 42 0 21 0 32.5984 0 21 0 9.40202 72 0 32.2355 0 72 0 111.766 0 144 21 144 9.40202 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="7.29112e-304" gWidth="7.29112e-304" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="100" ImageRes="1" GROUPS="1 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="17" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 100 0 100 0 100 0 100 0 100 17 100 17 100 17 100 17 0 17 0 17 0 17 0 17 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="7.29112e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="7.29112e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="107" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="524" NUMCO="16" POCOOR="0 0 0 0 100 0 100 0 100 0 100 0 100 17 100 17 100 17 100 17 0 17 0 17 0 17 0 17 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT CAB="0" CCOLOR="Black" CBASE="0" CSTW="-0.1" CSIZE="12" CULP="-0.1" CH="Agent (Resource)" CSHADE2="100" CKERN="0" CSCALE="100" CSHADE="100" COUT="1" CSCALEV="100" CSTP="-0.1" CULW="-0.1" CSTYLE="0" CFONT="Bitstream Charter Bold Italic" CSHX="5" CSHY="-5" CSTROKE="Black" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="5.00662e-308" gWidth="1.60219e-306" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="144" ImageRes="1" GROUPS="3 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Aquamarine3" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="1" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="42" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="144 21 144 32.5984 72 42 111.766 42 72 42 32.2355 42 0 21 0 32.5984 0 21 0 9.40202 72 0 32.2355 0 72 0 111.766 0 144 21 144 9.40202 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="1.11261e-306" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="4.4505e-308" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="156" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="501" NUMCO="16" POCOOR="144 21 144 32.5984 72 42 111.766 42 72 42 32.2355 42 0 21 0 32.5984 0 21 0 9.40202 72 0 32.2355 0 72 0 111.766 0 144 21 144 9.40202 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="5.33666e-315" gWidth="5.32028e-315" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="100" ImageRes="1" GROUPS="3 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="17" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 100 0 100 0 100 0 100 0 100 17 100 17 100 17 100 17 0 17 0 17 0 17 0 17 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="5.33663e-315" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="5.31963e-315" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="169" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="523" NUMCO="16" POCOOR="0 0 0 0 100 0 100 0 100 0 100 0 100 17 100 17 100 17 100 17 0 17 0 17 0 17 0 17 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT CAB="0" CCOLOR="Black" CBASE="0" CSTW="-0.1" CSIZE="12" CULP="-0.1" CH="Agent (Resource)" CSHADE2="100" CKERN="0" CSCALE="100" CSHADE="100" COUT="1" CSCALEV="100" CSTP="-0.1" CULW="-0.1" CSTYLE="0" CFONT="Bitstream Charter Bold Italic" CSHX="5" CSHY="-5" CSTROKE="Black" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="-9.88736e-117" gWidth="1.86808e-312" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="107" ImageRes="1" GROUPS="4 6 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="CadetBlue3" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="2" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="47" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 107 0 107 0 107 0 107 0 107 47 107 47 107 47 107 47 0 47 0 47 0 47 0 47 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="2.122e-314" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="9.34202e+20" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="33" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="330" NUMCO="16" POCOOR="0 0 0 0 107 0 107 0 107 0 107 0 107 47 107 47 107 47 107 47 0 47 0 47 0 47 0 47 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="1" gHeight="-1.79417e+308" gWidth="-1.79417e+308" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="68" ImageRes="1" GROUPS="4 6 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="2" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="20" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 68 0 68 0 68 0 68 0 68 20 68 20 68 20 68 20 0 20 0 20 0 20 0 20 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="-1.79417e+308" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="-1.79417e+308" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="47" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="350" NUMCO="16" POCOOR="0 0 0 0 68 0 68 0 68 0 68 0 68 20 68 20 68 20 68 20 0 20 0 20 0 20 0 20 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT CAB="1" CCOLOR="Black" CBASE="0" CSTW="-0.1" CSIZE="12" CULP="-0.1" CH="Control" CSHADE2="100" CKERN="0" CSCALE="100" CSHADE="100" COUT="1" CSCALEV="100" CSTP="-0.1" CULW="-0.1" CSTYLE="0" CFONT="Bitstream Charter Bold Italic" CSHX="5" CSHY="-5" CSTROKE="Black" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="5.31181e-315" gWidth="4.23076e+123" LANGUAGE="German" NUMPO="0" PLINEART="2" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="4.17044" TXTSHX="5" TXTSTROKE="Black" WIDTH="96.2549" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="0" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="5" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="1" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="" BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="-2" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="-5.1488e-247" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="107" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="405" NUMCO="0" POCOOR="" EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="7.29112e-304" gWidth="7.29112e-304" LANGUAGE="German" NUMPO="0" PLINEART="2" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="29.1406" TXTSHX="5" TXTSTROKE="Black" WIDTH="129.375" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="0" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="5" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="1" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="" BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="7.29112e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="7.29112e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="107" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="389" NUMCO="0" POCOOR="" EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="7.29112e-304" gWidth="7.29112e-304" LANGUAGE="German" NUMPO="0" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="141.18" TXTSHX="5" TXTSTROKE="Black" WIDTH="111.665" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="-43.5" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="0" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="5" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="1" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="" BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="7.29112e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="7.29112e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="118" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="502" NUMCO="0" POCOOR="" EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="8.47917e-305" gWidth="8.47917e-305" LANGUAGE="German" NUMPO="0" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="170.272" TXTSHX="5" TXTSTROKE="Black" WIDTH="71.0211" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="0" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="5" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="1" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="" BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="8.47917e-305" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="8.47917e-305" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="179" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="502" NUMCO="0" POCOOR="" EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="7.29112e-304" gWidth="7.29112e-304" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="93" ImageRes="1" GROUPS="6 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="CadetBlue2" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="26" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 93 0 93 0 93 0 93 0 93 26 93 26 93 26 93 26 0 26 0 26 0 26 0 26 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="7.29112e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="7.29112e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="80" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="337" NUMCO="16" POCOOR="0 0 0 0 93 0 93 0 93 0 93 0 93 26 93 26 93 26 93 26 0 26 0 26 0 26 0 26 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="7.29112e-304" gWidth="7.29112e-304" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="86" ImageRes="1" GROUPS="6 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="19" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 86 0 86 0 86 0 86 0 86 19 86 19 86 19 86 19 0 19 0 19 0 19 0 19 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="7.29112e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="9.9611e-307" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="84" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="342" NUMCO="16" POCOOR="0 0 0 0 86 0 86 0 86 0 86 0 86 19 86 19 86 19 86 19 0 19 0 19 0 19 0 19 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT CAB="0" CCOLOR="Black" CBASE="0" CSTW="-0.1" CSIZE="12" CULP="-0.1" CH="DBus-Interface" CSHADE2="100" CKERN="0" CSCALE="100" CSHADE="100" COUT="1" CSCALEV="100" CSTP="-0.1" CULW="-0.1" CSTYLE="0" CFONT="Bitstream Charter Bold Italic" CSHX="5" CSHY="-5" CSTROKE="Black" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="1" gHeight="5.00362e-304" gWidth="5.00362e-304" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="134" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="0" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="18" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 134 0 134 0 134 0 134 0 134 18 134 18 134 18 134 18 0 18 0 18 0 18 0 18 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="5.00362e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="5.00362e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="379" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="317" NUMCO="16" POCOOR="0 0 0 0 134 0 134 0 134 0 134 0 134 18 134 18 134 18 134 18 0 18 0 18 0 18 0 18 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT CAB="1" CCOLOR="Black" CBASE="0" CSTW="-0.1" CSIZE="12" CULP="-0.1" CH="Akonadi Service" CSHADE2="100" CKERN="0" CSCALE="100" CSHADE="100" COUT="1" CSCALEV="100" CSTP="-0.1" CULW="-0.1" CSTYLE="0" CFONT="Bitstream Charter Bold Italic" CSHX="5" CSHY="-5" CSTROKE="Black" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="1.64966e-304" gWidth="1.64966e-304" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="101" ImageRes="1" GROUPS="9 58 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Bisque2" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="2" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="0.962963" HEIGHT="25" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 101 0 101 0 101 0 101 0 101 25 101 25 101 25 101 25 0 25 0 25 0 25 0 25 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="1.64966e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="1.64966e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="189" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="333" NUMCO="16" POCOOR="0 0 0 0 101 0 101 0 101 0 101 0 101 25 101 25 101 25 101 25 0 25 0 25 0 25 0 25 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="338" gWidth="2.65638e-314" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="93" ImageRes="1" GROUPS="9 58 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="2" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="0.962963" HEIGHT="13.8889" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 93 0 93 0 93 0 93 0 93 13.8889 93 13.8889 93 13.8889 93 13.8889 0 13.8889 0 13.8889 0 13.8889 0 13.8889 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="5.34287e-315" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="1.4822e-323" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="194.556" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="338" NUMCO="16" POCOOR="0 0 0 0 93 0 93 0 93 0 93 0 93 13.8889 93 13.8889 93 13.8889 93 13.8889 0 13.8889 0 13.8889 0 13.8889 0 13.8889 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT CAB="0" CCOLOR="Black" CBASE="0" CSTW="-0.1" CSIZE="12" CULP="-0.1" CH="IMAP-Interface" CSHADE2="100" CKERN="0" CSCALE="100" CSHADE="100" COUT="1" CSCALEV="100" CSTP="-0.1" CULW="-0.1" CSTYLE="0" CFONT="Bitstream Charter Bold Italic" CSHX="5" CSHY="-5" CSTROKE="Black" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="-1.79417e+308" gWidth="-1.79417e+308" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="74" ImageRes="1" GROUPS="11 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Azure3" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="1" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="70" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="74 35 74 54.3307 37 70 57.4353 70 37 70 16.5655 70 0 35 0 54.3307 0 35 0 15.67 37 0 16.5655 0 37 0 57.4353 0 74 35 74 15.67 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="-1.79417e+308" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="-1.79417e+308" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="224" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="525" NUMCO="16" POCOOR="74 35 74 54.3307 37 70 57.4353 70 37 70 16.5655 70 0 35 0 54.3307 0 35 0 15.67 37 0 16.5655 0 37 0 57.4353 0 74 35 74 15.67 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="1" gHeight="3.63841e-304" gWidth="3.63841e-304" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="50" ImageRes="1" GROUPS="11 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="34" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 50 0 50 0 50 0 50 0 50 34 50 34 50 34 50 34 0 34 0 34 0 34 0 34 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="3.63841e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="3.63841e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="243" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="539" NUMCO="16" POCOOR="0 0 0 0 50 0 50 0 50 0 50 0 50 34 50 34 50 34 50 34 0 34 0 34 0 34 0 34 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT CAB="1" CCOLOR="Black" CBASE="0" CSTW="-0.1" CSIZE="12" CULP="-0.1" CH="Search-&#x5;Provider" CSHADE2="100" CKERN="0" CSCALE="100" CSHADE="100" COUT="1" CSCALEV="100" CSTP="-0.1" CULW="-0.1" CSTYLE="0" CFONT="Bitstream Charter Bold Italic" CSHX="5" CSHY="-5" CSTROKE="Black" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="1.78021e-306" gWidth="1.61324e-307" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="74" ImageRes="1" GROUPS="23 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Azure3" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="1" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="70" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="74 35 74 54.3307 37 70 57.4353 70 37 70 16.5655 70 0 35 0 54.3307 0 35 0 15.67 37 0 16.5655 0 37 0 57.4353 0 74 35 74 15.67 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="3.33767e-307" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="9.45699e-308" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="306" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="526" NUMCO="16" POCOOR="74 35 74 54.3307 37 70 57.4353 70 37 70 16.5655 70 0 35 0 54.3307 0 35 0 15.67 37 0 16.5655 0 37 0 57.4353 0 74 35 74 15.67 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="1" gHeight="4.4505e-308" gWidth="3.44898e-307" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="50" ImageRes="1" GROUPS="23 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="34" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 50 0 50 0 50 0 50 0 50 34 50 34 50 34 50 34 0 34 0 34 0 34 0 34 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="1.3908e-308" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="5.00662e-308" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="325" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="540" NUMCO="16" POCOOR="0 0 0 0 50 0 50 0 50 0 50 0 50 34 50 34 50 34 50 34 0 34 0 34 0 34 0 34 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT CAB="1" CCOLOR="Black" CBASE="0" CSTW="-0.1" CSIZE="12" CULP="-0.1" CH="Search-&#x5;Provider" CSHADE2="100" CKERN="0" CSCALE="100" CSHADE="100" COUT="1" CSCALEV="100" CSTP="-0.1" CULW="-0.1" CSTYLE="0" CFONT="Bitstream Charter Bold Italic" CSHX="5" CSHY="-5" CSTROKE="Black" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="2.122e-314" gWidth="0" LANGUAGE="German" NUMPO="0" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="28.0092" TXTSHX="5" TXTSTROKE="Black" WIDTH="106.471" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="0" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="5" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="1" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="" BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="2.122e-314" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="0" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="197" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="433" NUMCO="0" POCOOR="" EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="7.29112e-304" gWidth="1.78019e-306" LANGUAGE="German" NUMPO="0" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="51.1061" TXTSHX="5" TXTSTROKE="Black" WIDTH="152.895" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="0" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="5" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="1" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="" BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="1.60219e-306" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="7.56604e-307" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="205" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="434" NUMCO="0" POCOOR="" EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="-1.79417e+308" gWidth="-1.79417e+308" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="46" ImageRes="1" GROUPS="24 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="DarkSeaGreen3" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="0.945808" HEIGHT="169" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="1" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 46 0 46 0 46 0 46 0 46 169 46 169 46 169 46 169 0 169 0 169 0 169 0 169 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="-1.79417e+308" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="-1.79417e+308" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="71" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="193" NUMCO="16" POCOOR="0 0 0 0 46 0 46 0 46 0 46 0 46 169 46 169 46 169 46 169 0 169 0 169 0 169 0 169 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="5.32859e-266" gWidth="4.24399e-313" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="-90" TXTSHX="5" TXTSTROKE="Black" WIDTH="64.2552" ImageRes="1" GROUPS="24 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="0.945808" HEIGHT="19" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="1" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 70.2098 1.71522e-14 70.2098 1.71522e-14 70.2098 1.71522e-14 70.2098 1.71522e-14 70.2098 17.3886 70.2098 17.3886 70.2098 17.3886 70.2098 17.3886 -4.29652e-15 17.3886 -4.29652e-15 17.3886 -4.29652e-15 17.3886 -4.29652e-15 17.3886 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="3.61473e-313" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="7.35819e-316" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="186.307" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="206" NUMCO="16" POCOOR="0 0 0 0 64.2552 1.3947e-15 64.2552 1.3947e-15 64.2552 1.3947e-15 64.2552 1.3947e-15 64.2552 19 64.2552 19 64.2552 19 64.2552 19 0 19 0 19 0 19 0 19 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT CAB="0" CCOLOR="Black" CBASE="0" CSTW="-0.1" CSIZE="12" CULP="-0.1" CH="libakonadi" CSHADE2="100" CKERN="0" CSCALE="100" CSHADE="100" COUT="1" CSCALEV="100" CSTP="-0.1" CULW="-0.1" CSTYLE="0" CFONT="Bitstream Charter Bold Italic" CSHX="5" CSHY="-5" CSTROKE="Black" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="7.29112e-304" gWidth="7.29112e-304" LANGUAGE="German" NUMPO="0" PLINEART="2" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="-11.8887" TXTSHX="5" TXTSTROKE="Black" WIDTH="116.499" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="0" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="5" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="1" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="" BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="7.29112e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="7.29112e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="131" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="239" NUMCO="0" POCOOR="" EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="7.29112e-304" gWidth="7.29112e-304" LANGUAGE="German" NUMPO="0" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="19.5367" TXTSHX="5" TXTSTROKE="Black" WIDTH="98.6813" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="0" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="5" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="1" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="" BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="7.29112e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="7.29112e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="169" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="239" NUMCO="0" POCOOR="" EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="2.03712e-312" gWidth="4.5905e-268" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="31" ImageRes="1" GROUPS="52 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Aquamarine4" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="72.7448" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 31 0 31 0 31 0 31 0 31 72.7448 31 72.7448 31 72.7448 31 72.7448 0 72.7448 0 72.7448 0 72.7448 0 72.7448 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="7.31898e-267" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="0" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="83" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="162" NUMCO="16" POCOOR="0 0 0 0 31 0 31 0 31 0 31 0 31 72.7448 31 72.7448 31 72.7448 31 72.7448 0 72.7448 0 72.7448 0 72.7448 0 72.7448 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="1" gHeight="7.29112e-304" gWidth="7.29112e-304" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="-90" TXTSHX="5" TXTSTROKE="Black" WIDTH="48" ImageRes="1" GROUPS="52 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="18" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 48 0 48 0 48 0 48 0 48 18 48 18 48 18 48 18 0 18 0 18 0 18 0 18 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="7.29112e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="7.29112e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="142" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="168" NUMCO="16" POCOOR="0 0 0 0 48 0 48 0 48 0 48 0 48 18 48 18 48 18 48 18 0 18 0 18 0 18 0 18 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT CAB="1" CCOLOR="Black" CBASE="0" CSTW="-0.1" CSIZE="12" CULP="-0.1" CH="libkcal" CSHADE2="100" CKERN="0" CSCALE="100" CSHADE="100" COUT="1" CSCALEV="100" CSTP="-0.1" CULW="-0.1" CSTYLE="0" CFONT="Bitstream Charter Bold Italic" CSHX="5" CSHY="-5" CSTROKE="Black" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="1.61328e-307" gWidth="1.6912e-306" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="31" ImageRes="1" GROUPS="53 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Aquamarine4" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="72.7448" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 31 0 31 0 31 0 31 0 31 72.7448 31 72.7448 31 72.7448 31 72.7448 0 72.7448 0 72.7448 0 72.7448 0 72.7448 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="4.00524e-307" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="3.11524e-307" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="156" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="162" NUMCO="16" POCOOR="0 0 0 0 31 0 31 0 31 0 31 0 31 72.7448 31 72.7448 31 72.7448 31 72.7448 0 72.7448 0 72.7448 0 72.7448 0 72.7448 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="1" gHeight="5.28469e-308" gWidth="5.56295e-307" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="-90" TXTSHX="5" TXTSTROKE="Black" WIDTH="48" ImageRes="1" GROUPS="53 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="18" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 48 0 48 0 48 0 48 0 48 18 48 18 48 18 48 18 0 18 0 18 0 18 0 18 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="5.00662e-308" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="1.78021e-306" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="215" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="168" NUMCO="16" POCOOR="0 0 0 0 48 0 48 0 48 0 48 0 48 18 48 18 48 18 48 18 0 18 0 18 0 18 0 18 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT CAB="1" CCOLOR="Black" CBASE="0" CSTW="-0.1" CSIZE="12" CULP="-0.1" CH="libkabc" CSHADE2="100" CKERN="0" CSCALE="100" CSHADE="100" COUT="1" CSCALEV="100" CSTP="-0.1" CULW="-0.1" CSTYLE="0" CFONT="Bitstream Charter Bold Italic" CSHX="5" CSHY="-5" CSTROKE="Black" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="-1.79417e+308" gWidth="-1.79417e+308" LANGUAGE="German" NUMPO="48" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="92" ImageRes="1" GROUPS="55 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Azure" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="5" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="55" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="1" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 13.75 0 13.75 23 13.75 23 13.75 23 13.75 23 13.75 23 0 23 0 23 0 23 0 69 0 69 0 69 0 69 0 69 13.75 69 13.75 69 13.75 69 13.75 92 13.75 92 13.75 92 13.75 92 13.75 92 41.25 92 41.25 92 41.25 92 41.25 69 41.25 69 41.25 69 41.25 69 41.25 69 55 69 55 69 55 69 55 23 55 23 55 23 55 23 55 23 41.25 23 41.25 23 41.25 23 41.25 0 41.25 0 41.25 0 41.25 0 41.25 0 13.75 0 13.75 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="-1.79417e+308" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="-1.79417e+308" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="286" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="108" NUMCO="48" POCOOR="0 13.75 0 13.75 23 13.75 23 13.75 23 13.75 23 13.75 23 0 23 0 23 0 23 0 69 0 69 0 69 0 69 0 69 13.75 69 13.75 69 13.75 69 13.75 92 13.75 92 13.75 92 13.75 92 13.75 92 41.25 92 41.25 92 41.25 92 41.25 69 41.25 69 41.25 69 41.25 69 41.25 69 55 69 55 69 55 69 55 23 55 23 55 23 55 23 55 23 41.25 23 41.25 23 41.25 23 41.25 0 41.25 0 41.25 0 41.25 0 41.25 0 13.75 0 13.75 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="1" gHeight="7.29112e-304" gWidth="7.29112e-304" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="72" ImageRes="1" GROUPS="55 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="23" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 72 0 72 0 72 0 72 0 72 23 72 23 72 23 72 23 0 23 0 23 0 23 0 23 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="7.29112e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="7.29112e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="306" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="116" NUMCO="16" POCOOR="0 0 0 0 72 0 72 0 72 0 72 0 72 23 72 23 72 23 72 23 0 23 0 23 0 23 0 23 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT CAB="1" CCOLOR="Black" CBASE="0" CSTW="-0.1" CSIZE="12" CULP="-0.1" CH="Application" CSHADE2="100" CKERN="0" CSCALE="100" CSHADE="100" COUT="1" CSCALEV="100" CSTP="-0.1" CULW="-0.1" CSTYLE="0" CFONT="Bitstream Charter Bold Italic" CSHX="5" CSHY="-5" CSTROKE="Black" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="1.60219e-306" gWidth="7.56604e-307" LANGUAGE="German" NUMPO="48" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="92" ImageRes="1" GROUPS="56 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Azure4" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="5" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="55" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="1" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 13.75 0 13.75 23 13.75 23 13.75 23 13.75 23 13.75 23 0 23 0 23 0 23 0 69 0 69 0 69 0 69 0 69 13.75 69 13.75 69 13.75 69 13.75 92 13.75 92 13.75 92 13.75 92 13.75 92 41.25 92 41.25 92 41.25 92 41.25 69 41.25 69 41.25 69 41.25 69 41.25 69 55 69 55 69 55 69 55 23 55 23 55 23 55 23 55 23 41.25 23 41.25 23 41.25 23 41.25 0 41.25 0 41.25 0 41.25 0 41.25 0 13.75 0 13.75 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="9.25535e-266" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="5.73167e-266" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="343" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="168" NUMCO="48" POCOOR="0 13.75 0 13.75 23 13.75 23 13.75 23 13.75 23 13.75 23 0 23 0 23 0 23 0 69 0 69 0 69 0 69 0 69 13.75 69 13.75 69 13.75 69 13.75 92 13.75 92 13.75 92 13.75 92 13.75 92 41.25 92 41.25 92 41.25 92 41.25 69 41.25 69 41.25 69 41.25 69 41.25 69 55 69 55 69 55 69 55 23 55 23 55 23 55 23 55 23 41.25 23 41.25 23 41.25 23 41.25 0 41.25 0 41.25 0 41.25 0 41.25 0 13.75 0 13.75 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="1" gHeight="5.79445e-266" gWidth="0" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="72" ImageRes="1" GROUPS="56 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="23" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 72 0 72 0 72 0 72 0 72 23 72 23 72 23 72 23 0 23 0 23 0 23 0 23 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="5.79525e-266" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="1.5279e-311" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="363" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="176" NUMCO="16" POCOOR="0 0 0 0 72 0 72 0 72 0 72 0 72 23 72 23 72 23 72 23 0 23 0 23 0 23 0 23 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT CAB="1" CCOLOR="Black" CBASE="0" CSTW="-0.1" CSIZE="12" CULP="-0.1" CH="Component" CSHADE2="100" CKERN="0" CSCALE="100" CSHADE="100" COUT="1" CSCALEV="100" CSTP="-0.1" CULW="-0.1" CSTYLE="0" CFONT="Bitstream Charter Bold Italic" CSHX="5" CSHY="-5" CSTROKE="Black" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="2.03712e-312" gWidth="7.39983e-316" LANGUAGE="German" NUMPO="0" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="-81.3087" TXTSHX="5" TXTSTROKE="Black" WIDTH="158.824" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="-78.5" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="0" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="5" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="1" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="" BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="1.13933e-265" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="8.48798e-314" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="286" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="137" NUMCO="0" POCOOR="" EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="2.05323e-312" gWidth="1.51974e-100" LANGUAGE="German" NUMPO="0" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="-114.52" TXTSHX="5" TXTSTROKE="Black" WIDTH="125.3" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="0" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="5" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="1" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="" BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="-6.61593e+41" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="-1.5154e+260" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="343" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="225" NUMCO="0" POCOOR="" EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="1.0936e-305" gWidth="1.0936e-305" LANGUAGE="German" NUMPO="16" PLINEART="1" TXTSCALE="100" RightLine="0" LOCALSCX="1" LINESP="10.8" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="91" ImageRes="1" GROUPS="57 58 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="2" TransValue="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="69" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="9" PLTSHOW="0" LINESPMode="0" TXTSTYLE="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 91 0 91 0 91 0 91 0 91 69 91 69 91 69 91 69 0 69 0 69 0 69 0 69 0 0 0 0 " BASEOF="0" PICART="1" TXTKERN="0" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="1.0936e-305" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="1.0936e-305" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="258" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="339" NUMCO="16" POCOOR="0 0 0 0 91 0 91 0 91 0 91 0 91 69 91 69 91 69 91 69 0 69 0 69 0 69 0 69 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT CAB="0" CCOLOR="Black" CBASE="0" CSTW="-0.1" CSIZE="9" CULP="-0.1" CH=" /&#x5; /resource1/contacts&#x5;/resource1/events&#x5;/resource2&#x5;/search" CSHADE2="100" CKERN="0" CSCALE="100" CSHADE="100" COUT="1" CSCALEV="100" CSTP="-0.1" CULW="-0.1" CSTYLE="0" CFONT="Bitstream Charter Bold Italic" CSHX="5" CSHY="-5" CSTROKE="Black" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+ </DOCUMENT>
+</SCRIBUSUTF8NEW>
diff --git a/doc/pics/akonadi_overview_uml.png b/doc/pics/akonadi_overview_uml.png
new file mode 100644 (file)
index 0000000..70817a6
Binary files /dev/null and b/doc/pics/akonadi_overview_uml.png differ
diff --git a/doc/pics/akonadi_overview_uml.ps b/doc/pics/akonadi_overview_uml.ps
new file mode 100644 (file)
index 0000000..91f3cc0
--- /dev/null
@@ -0,0 +1,2551 @@
+%!PS-Adobe-3.0
+%%BeginProcSet: reencode 1.0 0
+/RE
+{  findfont begin
+  currentdict dup length dict begin
+  {1 index /FID ne {def} {pop pop} ifelse} forall
+  /FontName exch def dup length 0 ne
+  { /Encoding Encoding 256 array copy def
+      0 exch
+      { dup type /nametype eq
+        { Encoding 2 index 2 index put
+          pop 1 add
+        }
+        { exch pop
+        } ifelse
+      } forall
+  } if pop
+  currentdict dup end end
+  /FontName get exch definefont pop
+    } bind def
+%%EndProcSet: reencode 1.0 0
+%%BeginProcSet: ellipse 1.0 0
+/ellipsedict 8 dict def
+ellipsedict /mtrx matrix put
+/ellipse { ellipsedict begin
+/endangle exch def
+/startangle exch def
+/yrad exch def
+/xrad exch def
+/y exch def
+/x exch def
+/savematrix mtrx currentmatrix def
+x y translate
+xrad yrad scale
+0 0 1 0 360 arc
+savematrix setmatrix end } def
+%%EndProcSet: ellipse 1.0 0
+%%EndProlog
+%%BeginSetup
+/isolatin1encoding
+[ 32 /space /exclam /quotedbl /numbersign /dollar /percent /ampersand /quoteright
+ /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash /zero /one
+ /two /three /four /five /six /seven /eight /nine /colon /semicolon
+ /less /equal /greater /question /at /A /B /C /D /E
+ /F /G /H /I /J /K /L /M /N /O
+ /P /Q /R /S /T /U /V /W /X /Y
+ /Z /bracketleft /backslash /bracketright /asciicircum /underscore /quoteleft /a /b /c
+ /d /e /f /g /h /i /j /k /l /m
+ /n /o /p /q /r /s /t /u /v /w
+ /x /y /z /braceleft /bar /braceright /asciitilde /.notdef /.notdef /.notdef
+ /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef
+ /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef
+ /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef /.notdef
+ /space /exclamdown /cent /sterling /currency /yen /brokenbar /section /dieresis /copyright
+ /ordfeminine /guillemotleft /logicalnot /hyphen /registered /macron /degree /plusminus /twosuperior /threesuperior
+ /acute /mu /paragraph /periodcentered /cedilla /onesuperior /ordmasculine /guillemotright /onequarter /onehalf
+ /threequarters /questiondown /Agrave /Aacute /Acircumflex /Atilde /Adieresis /Aring /AE /Ccedilla
+ /Egrave /Eacute /Ecircumflex /Edieresis /Igrave /Iacute /Icircumflex /Idieresis /Eth /Ntilde
+ /Ograve /Oacute /Ocircumflex /Otilde /Odieresis /multiply /Oslash /Ugrave /Uacute /Ucircumflex
+ /Udieresis /Yacute /Thorn /germandbls /agrave /aacute /acircumflex /atilde /adieresis /aring
+ /ae /ccedilla /egrave /eacute /ecircumflex /edieresis /igrave /iacute /icircumflex /idieresis
+ /eth /ntilde /ograve /oacute /ocircumflex /otilde /odieresis /divide /oslash /ugrave
+ /uacute /ucircumflex /udieresis /yacute /thorn /ydieresis] def
+%%EndSetup
+1 setlinewidth
+isolatin1encoding /_Helvetica /Helvetica RE
+/_Helvetica findfont
+12 scalefont setfont
+0.0 0.0 0.0 setrgbcolor
+32 810 translate
+0.625 0.625 scale
+-20 -12 translate
+newpath
+20 -12 moveto
+855 0 rlineto
+0 -1195 rlineto
+-855 0 rlineto
+closepath
+clip
+1.0 1.0 1.0 setrgbcolor
+newpath
+24 -640 moveto
+798 0 rlineto
+0 -20 rlineto
+-798 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+24 -640 moveto
+798 0 rlineto
+0 -20 rlineto
+-798 0 rlineto
+closepath
+stroke
+isolatin1encoding /_Helvetica /Helvetica RE
+/_Helvetica findfont
+10 scalefont setfont
+28 -653 moveto
+(server) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+24 -660 moveto
+847 0 rlineto
+0 -459 rlineto
+-847 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+24 -660 moveto
+847 0 rlineto
+0 -459 rlineto
+-847 0 rlineto
+closepath
+stroke
+isolatin1encoding /_TimesRoman /TimesRoman RE
+/_TimesRoman findfont
+10 scalefont setfont
+1.0 1.0 1.0 setrgbcolor
+newpath
+392 -920 moveto
+390 0 rlineto
+0 -20 rlineto
+-390 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+392 -920 moveto
+390 0 rlineto
+0 -20 rlineto
+-390 0 rlineto
+closepath
+stroke
+isolatin1encoding /_Helvetica /Helvetica RE
+/_Helvetica findfont
+10 scalefont setfont
+396 -933 moveto
+(storage) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+392 -940 moveto
+439 0 rlineto
+0 -147 rlineto
+-439 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+392 -940 moveto
+439 0 rlineto
+0 -147 rlineto
+-439 0 rlineto
+closepath
+stroke
+isolatin1encoding /_TimesRoman /TimesRoman RE
+/_TimesRoman findfont
+10 scalefont setfont
+1.0 1.0 1.0 setrgbcolor
+newpath
+488 -1032 moveto
+95 0 rlineto
+0 -45 rlineto
+-95 0 rlineto
+closepath
+eofill
+newpath
+488 -1032 moveto
+95 0 rlineto
+0 -1 rlineto
+-95 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+488 -1032 moveto
+96 0 rlineto
+0 -2 rlineto
+-96 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+488 -1034 moveto
+95 0 rlineto
+0 -19 rlineto
+-95 0 rlineto
+closepath
+eofill
+isolatin1encoding /_Helvetica /Helvetica RE
+/_Helvetica findfont
+10 scalefont setfont
+0.0 0.0 0.0 setrgbcolor
+512 -1047 moveto
+(DataStore) show
+newpath
+488 -1032 moveto
+96 0 rlineto
+0 -46 rlineto
+-96 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+408 -944 moveto
+108 0 rlineto
+0 -21 rlineto
+-108 0 rlineto
+closepath
+eofill
+newpath
+408 -944 moveto
+108 0 rlineto
+0 -1 rlineto
+-108 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+408 -944 moveto
+109 0 rlineto
+0 -2 rlineto
+-109 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+408 -946 moveto
+108 0 rlineto
+0 -19 rlineto
+-108 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+412 -959 moveto
+(NotificationCollector) show
+newpath
+408 -944 moveto
+109 0 rlineto
+0 -22 rlineto
+-109 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+736 -1048 moveto
+67 0 rlineto
+0 -21 rlineto
+-67 0 rlineto
+closepath
+eofill
+newpath
+736 -1048 moveto
+67 0 rlineto
+0 -1 rlineto
+-67 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+736 -1048 moveto
+68 0 rlineto
+0 -2 rlineto
+-68 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+736 -1050 moveto
+67 0 rlineto
+0 -19 rlineto
+-67 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+740 -1063 moveto
+(DbInitializer) show
+newpath
+736 -1048 moveto
+68 0 rlineto
+0 -22 rlineto
+-68 0 rlineto
+closepath
+stroke
+[5.0 5.0 ] 0 setdash
+newpath
+520 -1032 moveto
+520 -984 lineto
+stroke
+newpath
+520 -984 moveto
+517 -966 lineto
+stroke
+[] 0 setdash
+newpath
+525 -976 moveto
+517 -966 lineto
+stroke
+newpath
+512 -978 moveto
+517 -966 lineto
+stroke
+545 -1005 moveto
+(send changes) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+656 -1144 moveto
+88 0 rlineto
+0 -59 rlineto
+-88 0 rlineto
+closepath
+eofill
+newpath
+656 -1144 moveto
+88 0 rlineto
+0 -14 rlineto
+-88 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+656 -1144 moveto
+89 0 rlineto
+0 -15 rlineto
+-89 0 rlineto
+closepath
+stroke
+662 -1158 moveto
+(<<interface>>) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+656 -1159 moveto
+88 0 rlineto
+0 -19 rlineto
+-88 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+660 -1172 moveto
+(MySQL Database) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+657 -1180 moveto
+87 0 rlineto
+0 -22 rlineto
+-87 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+657 -1180 moveto
+745 -1180 lineto
+stroke
+newpath
+656 -1144 moveto
+89 0 rlineto
+0 -60 rlineto
+-89 0 rlineto
+closepath
+stroke
+[5.0 5.0 ] 0 setdash
+newpath
+737 -1070 moveto
+737 -1144 lineto
+stroke
+[] 0 setdash
+newpath
+730 -1132 moveto
+737 -1144 lineto
+stroke
+newpath
+744 -1132 moveto
+737 -1144 lineto
+stroke
+[5.0 5.0 ] 0 setdash
+newpath
+584 -1078 moveto
+656 -1144 lineto
+stroke
+[] 0 setdash
+newpath
+642 -1141 moveto
+656 -1144 lineto
+stroke
+newpath
+651 -1130 moveto
+656 -1144 lineto
+stroke
+0.78431374 1.0 1.0 setrgbcolor
+newpath
+40 -864 moveto
+94 0 rlineto
+0 -21 rlineto
+-94 0 rlineto
+closepath
+eofill
+newpath
+40 -864 moveto
+94 0 rlineto
+0 -1 rlineto
+-94 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+40 -864 moveto
+95 0 rlineto
+0 -2 rlineto
+-95 0 rlineto
+closepath
+stroke
+0.78431374 1.0 1.0 setrgbcolor
+newpath
+40 -866 moveto
+94 0 rlineto
+0 -19 rlineto
+-94 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+44 -879 moveto
+(RecourceManager) show
+newpath
+40 -864 moveto
+95 0 rlineto
+0 -22 rlineto
+-95 0 rlineto
+closepath
+stroke
+[5.0 5.0 ] 0 setdash
+newpath
+584 -1056 moveto
+736 -1056 lineto
+stroke
+[] 0 setdash
+newpath
+724 -1063 moveto
+736 -1056 lineto
+stroke
+newpath
+724 -1049 moveto
+736 -1056 lineto
+stroke
+615 -1037 moveto
+(build db) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+696 -720 moveto
+102 0 rlineto
+0 -20 rlineto
+-102 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+696 -720 moveto
+102 0 rlineto
+0 -20 rlineto
+-102 0 rlineto
+closepath
+stroke
+700 -733 moveto
+(handler) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+696 -740 moveto
+151 0 rlineto
+0 -163 rlineto
+-151 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+696 -740 moveto
+151 0 rlineto
+0 -163 rlineto
+-151 0 rlineto
+closepath
+stroke
+isolatin1encoding /_TimesRoman /TimesRoman RE
+/_TimesRoman findfont
+10 scalefont setfont
+1.0 1.0 1.0 setrgbcolor
+newpath
+704 -744 moveto
+59 0 rlineto
+0 -21 rlineto
+-59 0 rlineto
+closepath
+eofill
+newpath
+704 -744 moveto
+59 0 rlineto
+0 -1 rlineto
+-59 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+704 -744 moveto
+60 0 rlineto
+0 -2 rlineto
+-60 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+704 -746 moveto
+59 0 rlineto
+0 -19 rlineto
+-59 0 rlineto
+closepath
+eofill
+isolatin1encoding /_Helvetica /Helvetica RE
+/_Helvetica findfont
+10 scalefont setfont
+0.0 0.0 0.0 setrgbcolor
+715 -759 moveto
+(Handler) show
+newpath
+704 -744 moveto
+60 0 rlineto
+0 -22 rlineto
+-60 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+736 -872 moveto
+59 0 rlineto
+0 -21 rlineto
+-59 0 rlineto
+closepath
+eofill
+newpath
+736 -872 moveto
+59 0 rlineto
+0 -1 rlineto
+-59 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+736 -872 moveto
+60 0 rlineto
+0 -2 rlineto
+-60 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+736 -874 moveto
+59 0 rlineto
+0 -19 rlineto
+-59 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+751 -887 moveto
+(Status) show
+newpath
+736 -872 moveto
+60 0 rlineto
+0 -22 rlineto
+-60 0 rlineto
+closepath
+stroke
+newpath
+744 -872 moveto
+744 -766 lineto
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+744 -766 moveto
+751 -778 lineto
+737 -778 lineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+744 -766 moveto
+751 -778 lineto
+737 -778 lineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+784 -800 moveto
+59 0 rlineto
+0 -21 rlineto
+-59 0 rlineto
+closepath
+eofill
+newpath
+784 -800 moveto
+59 0 rlineto
+0 -1 rlineto
+-59 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+784 -800 moveto
+60 0 rlineto
+0 -2 rlineto
+-60 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+784 -802 moveto
+59 0 rlineto
+0 -19 rlineto
+-59 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+795 -815 moveto
+(Append) show
+newpath
+784 -800 moveto
+60 0 rlineto
+0 -22 rlineto
+-60 0 rlineto
+closepath
+stroke
+newpath
+784 -800 moveto
+764 -766 lineto
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+764 -766 moveto
+776 -772 lineto
+764 -779 lineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+764 -766 moveto
+776 -772 lineto
+764 -779 lineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+768 -840 moveto
+59 0 rlineto
+0 -21 rlineto
+-59 0 rlineto
+closepath
+eofill
+newpath
+768 -840 moveto
+59 0 rlineto
+0 -1 rlineto
+-59 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+768 -840 moveto
+60 0 rlineto
+0 -2 rlineto
+-60 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+768 -842 moveto
+59 0 rlineto
+0 -19 rlineto
+-59 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+782 -855 moveto
+(Delete) show
+newpath
+768 -840 moveto
+60 0 rlineto
+0 -22 rlineto
+-60 0 rlineto
+closepath
+stroke
+newpath
+768 -840 moveto
+764 -766 lineto
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+764 -766 moveto
+771 -777 lineto
+757 -778 lineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+764 -766 moveto
+771 -777 lineto
+757 -778 lineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+536 -704 moveto
+102 0 rlineto
+0 -36 rlineto
+-102 0 rlineto
+closepath
+eofill
+newpath
+536 -704 moveto
+102 0 rlineto
+0 -14 rlineto
+-102 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+536 -704 moveto
+103 0 rlineto
+0 -15 rlineto
+-103 0 rlineto
+closepath
+stroke
+555 -718 moveto
+(<<thread>>) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+536 -719 moveto
+102 0 rlineto
+0 -19 rlineto
+-102 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+540 -732 moveto
+(AkonadiConnection) show
+newpath
+536 -704 moveto
+103 0 rlineto
+0 -37 rlineto
+-103 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+568 -840 moveto
+75 0 rlineto
+0 -36 rlineto
+-75 0 rlineto
+closepath
+eofill
+newpath
+568 -840 moveto
+75 0 rlineto
+0 -14 rlineto
+-75 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+568 -840 moveto
+76 0 rlineto
+0 -15 rlineto
+-76 0 rlineto
+closepath
+stroke
+574 -854 moveto
+(<<thread>>) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+568 -855 moveto
+75 0 rlineto
+0 -19 rlineto
+-75 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+572 -868 moveto
+(CacheCleaner) show
+newpath
+568 -840 moveto
+76 0 rlineto
+0 -37 rlineto
+-76 0 rlineto
+closepath
+stroke
+[5.0 5.0 ] 0 setdash
+newpath
+639 -728 moveto
+696 -728 lineto
+stroke
+[] 0 setdash
+newpath
+684 -735 moveto
+696 -728 lineto
+stroke
+newpath
+684 -721 moveto
+696 -728 lineto
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+368 -776 moveto
+183 0 rlineto
+0 -34 rlineto
+-183 0 rlineto
+closepath
+eofill
+newpath
+368 -776 moveto
+183 0 rlineto
+0 -14 rlineto
+-183 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+368 -776 moveto
+184 0 rlineto
+0 -15 rlineto
+-184 0 rlineto
+closepath
+stroke
+420 -790 moveto
+(<<TcpServer>>) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+368 -791 moveto
+183 0 rlineto
+0 -19 rlineto
+-183 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+425 -804 moveto
+(AkonadiServer) show
+newpath
+368 -776 moveto
+184 0 rlineto
+0 -35 rlineto
+-184 0 rlineto
+closepath
+stroke
+newpath
+368 -811 moveto
+135 -864 lineto
+stroke
+newpath
+368 -811 moveto
+359 -818 lineto
+349 -815 lineto
+357 -808 lineto
+closepath
+eofill
+newpath
+368 -811 moveto
+359 -818 lineto
+349 -815 lineto
+357 -808 lineto
+closepath
+stroke
+353 -833 moveto
+(1) show
+149 -879 moveto
+(1) show
+newpath
+552 -811 moveto
+568 -840 lineto
+stroke
+newpath
+552 -811 moveto
+561 -817 lineto
+561 -828 lineto
+552 -822 lineto
+closepath
+eofill
+newpath
+552 -811 moveto
+561 -817 lineto
+561 -828 lineto
+552 -822 lineto
+closepath
+stroke
+569 -822 moveto
+(1) show
+570 -824 moveto
+(1) show
+newpath
+536 -811 moveto
+536 -1032 lineto
+stroke
+newpath
+536 -811 moveto
+541 -821 lineto
+536 -831 lineto
+531 -821 lineto
+closepath
+eofill
+newpath
+536 -811 moveto
+541 -821 lineto
+536 -831 lineto
+531 -821 lineto
+closepath
+stroke
+548 -831 moveto
+(1) show
+548 -1022 moveto
+(1) show
+1.0 1.0 0.78431374 setrgbcolor
+newpath
+208 -760 moveto
+107 0 rlineto
+0 -21 rlineto
+-107 0 rlineto
+closepath
+eofill
+newpath
+208 -760 moveto
+107 0 rlineto
+0 -1 rlineto
+-107 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+208 -760 moveto
+108 0 rlineto
+0 -2 rlineto
+-108 0 rlineto
+closepath
+stroke
+1.0 1.0 0.78431374 setrgbcolor
+newpath
+208 -762 moveto
+107 0 rlineto
+0 -19 rlineto
+-107 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+212 -775 moveto
+(NotificationManager) show
+newpath
+208 -760 moveto
+108 0 rlineto
+0 -22 rlineto
+-108 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+336 -712 moveto
+108 0 rlineto
+0 -21 rlineto
+-108 0 rlineto
+closepath
+eofill
+newpath
+336 -712 moveto
+108 0 rlineto
+0 -1 rlineto
+-108 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+336 -712 moveto
+109 0 rlineto
+0 -2 rlineto
+-109 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+336 -714 moveto
+108 0 rlineto
+0 -19 rlineto
+-108 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+340 -727 moveto
+(CachePolicyManager) show
+newpath
+336 -712 moveto
+109 0 rlineto
+0 -22 rlineto
+-109 0 rlineto
+closepath
+stroke
+newpath
+376 -776 moveto
+376 -734 lineto
+stroke
+newpath
+376 -776 moveto
+371 -766 lineto
+376 -756 lineto
+381 -766 lineto
+closepath
+eofill
+newpath
+376 -776 moveto
+371 -766 lineto
+376 -756 lineto
+381 -766 lineto
+closepath
+stroke
+358 -766 moveto
+(1) show
+358 -754 moveto
+(1) show
+newpath
+368 -782 moveto
+316 -782 lineto
+stroke
+newpath
+368 -782 moveto
+358 -787 lineto
+348 -782 lineto
+358 -777 lineto
+closepath
+eofill
+newpath
+368 -782 moveto
+358 -787 lineto
+348 -782 lineto
+358 -777 lineto
+closepath
+stroke
+350 -802 moveto
+(1) show
+328 -802 moveto
+(1) show
+[5.0 5.0 ] 0 setdash
+newpath
+135 -886 moveto
+488 -1032 lineto
+stroke
+[] 0 setdash
+newpath
+474 -1033 moveto
+488 -1032 lineto
+stroke
+newpath
+479 -1020 moveto
+488 -1032 lineto
+stroke
+285 -942 moveto
+(keep DS up to date) show
+[5.0 5.0 ] 0 setdash
+newpath
+544 -776 moveto
+544 -741 lineto
+stroke
+[] 0 setdash
+newpath
+551 -753 moveto
+544 -741 lineto
+stroke
+newpath
+537 -753 moveto
+544 -741 lineto
+stroke
+575 -765 moveto
+(use) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+24 -496 moveto
+798 0 rlineto
+0 -20 rlineto
+-798 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+24 -496 moveto
+798 0 rlineto
+0 -20 rlineto
+-798 0 rlineto
+closepath
+stroke
+28 -509 moveto
+(server/control) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+24 -516 moveto
+847 0 rlineto
+0 -91 rlineto
+-847 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+24 -516 moveto
+847 0 rlineto
+0 -91 rlineto
+-847 0 rlineto
+closepath
+stroke
+isolatin1encoding /_TimesRoman /TimesRoman RE
+/_TimesRoman findfont
+10 scalefont setfont
+0.78431374 1.0 1.0 setrgbcolor
+newpath
+104 -576 moveto
+79 0 rlineto
+0 -21 rlineto
+-79 0 rlineto
+closepath
+eofill
+newpath
+104 -576 moveto
+79 0 rlineto
+0 -1 rlineto
+-79 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+104 -576 moveto
+80 0 rlineto
+0 -2 rlineto
+-80 0 rlineto
+closepath
+stroke
+0.78431374 1.0 1.0 setrgbcolor
+newpath
+104 -578 moveto
+79 0 rlineto
+0 -19 rlineto
+-79 0 rlineto
+closepath
+eofill
+isolatin1encoding /_Helvetica /Helvetica RE
+/_Helvetica findfont
+10 scalefont setfont
+0.0 0.0 0.0 setrgbcolor
+108 -591 moveto
+(AgentManager) show
+newpath
+104 -576 moveto
+80 0 rlineto
+0 -22 rlineto
+-80 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+304 -576 moveto
+82 0 rlineto
+0 -21 rlineto
+-82 0 rlineto
+closepath
+eofill
+newpath
+304 -576 moveto
+82 0 rlineto
+0 -1 rlineto
+-82 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+304 -576 moveto
+83 0 rlineto
+0 -2 rlineto
+-83 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+304 -578 moveto
+82 0 rlineto
+0 -19 rlineto
+-82 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+308 -591 moveto
+(ProfileManager) show
+newpath
+304 -576 moveto
+83 0 rlineto
+0 -22 rlineto
+-83 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+496 -576 moveto
+122 0 rlineto
+0 -21 rlineto
+-122 0 rlineto
+closepath
+eofill
+newpath
+496 -576 moveto
+122 0 rlineto
+0 -1 rlineto
+-122 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+496 -576 moveto
+123 0 rlineto
+0 -2 rlineto
+-123 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+496 -578 moveto
+122 0 rlineto
+0 -19 rlineto
+-122 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+500 -591 moveto
+(SearchProviderManager) show
+newpath
+496 -576 moveto
+123 0 rlineto
+0 -22 rlineto
+-123 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+632 -640 moveto
+81 0 rlineto
+0 -40 rlineto
+-81 0 rlineto
+closepath
+eofill
+newpath
+632 -640 moveto
+81 0 rlineto
+0 -14 rlineto
+-81 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+632 -640 moveto
+82 0 rlineto
+0 -15 rlineto
+-82 0 rlineto
+closepath
+stroke
+635 -654 moveto
+(<<interface>>) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+632 -655 moveto
+81 0 rlineto
+0 -19 rlineto
+-81 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+660 -668 moveto
+(IMAP) show
+newpath
+632 -640 moveto
+82 0 rlineto
+0 -41 rlineto
+-82 0 rlineto
+closepath
+stroke
+[5.0 5.0 ] 0 setdash
+newpath
+632 -704 moveto
+632 -681 lineto
+stroke
+1.0 1.0 1.0 setrgbcolor
+[] 0 setdash
+newpath
+632 -681 moveto
+639 -693 lineto
+625 -693 lineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+632 -681 moveto
+639 -693 lineto
+625 -693 lineto
+closepath
+stroke
+669 -706 moveto
+(<<realize>>) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+184 -640 moveto
+81 0 rlineto
+0 -40 rlineto
+-81 0 rlineto
+closepath
+eofill
+newpath
+184 -640 moveto
+81 0 rlineto
+0 -14 rlineto
+-81 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+184 -640 moveto
+82 0 rlineto
+0 -15 rlineto
+-82 0 rlineto
+closepath
+stroke
+187 -654 moveto
+(<<interface>>) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+184 -655 moveto
+81 0 rlineto
+0 -19 rlineto
+-81 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+213 -668 moveto
+(DBus) show
+newpath
+184 -640 moveto
+82 0 rlineto
+0 -41 rlineto
+-82 0 rlineto
+closepath
+stroke
+[5.0 5.0 ] 0 setdash
+newpath
+258 -760 moveto
+258 -681 lineto
+stroke
+1.0 1.0 1.0 setrgbcolor
+[] 0 setdash
+newpath
+258 -681 moveto
+265 -693 lineto
+251 -693 lineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+258 -681 moveto
+265 -693 lineto
+251 -693 lineto
+closepath
+stroke
+295 -734 moveto
+(<<realize>>) show
+[5.0 5.0 ] 0 setdash
+newpath
+135 -864 moveto
+184 -681 lineto
+stroke
+1.0 1.0 1.0 setrgbcolor
+[] 0 setdash
+newpath
+184 -681 moveto
+187 -694 lineto
+174 -690 lineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+184 -681 moveto
+187 -694 lineto
+174 -690 lineto
+closepath
+stroke
+194 -781 moveto
+(<<realize>>) show
+[5.0 5.0 ] 0 setdash
+newpath
+336 -712 moveto
+266 -681 lineto
+stroke
+1.0 1.0 1.0 setrgbcolor
+[] 0 setdash
+newpath
+266 -681 moveto
+279 -679 lineto
+274 -692 lineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+266 -681 moveto
+279 -679 lineto
+274 -692 lineto
+closepath
+stroke
+296 -688 moveto
+(<<realize>>) show
+[5.0 5.0 ] 0 setdash
+newpath
+184 -598 moveto
+184 -640 lineto
+stroke
+[] 0 setdash
+newpath
+177 -628 moveto
+184 -640 lineto
+stroke
+newpath
+191 -628 moveto
+184 -640 lineto
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+336 -496 moveto
+81 0 rlineto
+0 -40 rlineto
+-81 0 rlineto
+closepath
+eofill
+newpath
+336 -496 moveto
+81 0 rlineto
+0 -14 rlineto
+-81 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+336 -496 moveto
+82 0 rlineto
+0 -15 rlineto
+-82 0 rlineto
+closepath
+stroke
+339 -510 moveto
+(<<interface>>) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+336 -511 moveto
+81 0 rlineto
+0 -19 rlineto
+-81 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+365 -524 moveto
+(DBus) show
+newpath
+336 -496 moveto
+82 0 rlineto
+0 -41 rlineto
+-82 0 rlineto
+closepath
+stroke
+[5.0 5.0 ] 0 setdash
+newpath
+184 -576 moveto
+336 -537 lineto
+stroke
+1.0 1.0 1.0 setrgbcolor
+[] 0 setdash
+newpath
+336 -537 moveto
+326 -546 lineto
+322 -533 lineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+336 -537 moveto
+326 -546 lineto
+322 -533 lineto
+closepath
+stroke
+241 -546 moveto
+(<<realize>>) show
+[5.0 5.0 ] 0 setdash
+newpath
+377 -576 moveto
+377 -537 lineto
+stroke
+1.0 1.0 1.0 setrgbcolor
+[] 0 setdash
+newpath
+377 -537 moveto
+384 -549 lineto
+370 -549 lineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+377 -537 moveto
+384 -549 lineto
+370 -549 lineto
+closepath
+stroke
+414 -570 moveto
+(<<realize>>) show
+[5.0 5.0 ] 0 setdash
+newpath
+496 -576 moveto
+418 -537 lineto
+stroke
+1.0 1.0 1.0 setrgbcolor
+[] 0 setdash
+newpath
+418 -537 moveto
+431 -536 lineto
+425 -548 lineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+418 -537 moveto
+431 -536 lineto
+425 -548 lineto
+closepath
+stroke
+453 -548 moveto
+(<<realize>>) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+32 -384 moveto
+158 0 rlineto
+0 -20 rlineto
+-158 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+32 -384 moveto
+158 0 rlineto
+0 -20 rlineto
+-158 0 rlineto
+closepath
+stroke
+36 -397 moveto
+(resources) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+32 -404 moveto
+207 0 rlineto
+0 -71 rlineto
+-207 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+32 -404 moveto
+207 0 rlineto
+0 -71 rlineto
+-207 0 rlineto
+closepath
+stroke
+isolatin1encoding /_TimesRoman /TimesRoman RE
+/_TimesRoman findfont
+10 scalefont setfont
+1.0 1.0 1.0 setrgbcolor
+newpath
+296 -384 moveto
+190 0 rlineto
+0 -20 rlineto
+-190 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+296 -384 moveto
+190 0 rlineto
+0 -20 rlineto
+-190 0 rlineto
+closepath
+stroke
+isolatin1encoding /_Helvetica /Helvetica RE
+/_Helvetica findfont
+10 scalefont setfont
+300 -397 moveto
+(searchprovider) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+296 -404 moveto
+239 0 rlineto
+0 -75 rlineto
+-239 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+296 -404 moveto
+239 0 rlineto
+0 -75 rlineto
+-239 0 rlineto
+closepath
+stroke
+isolatin1encoding /_TimesRoman /TimesRoman RE
+/_TimesRoman findfont
+10 scalefont setfont
+1.0 1.0 1.0 setrgbcolor
+newpath
+24 -128 moveto
+798 0 rlineto
+0 -20 rlineto
+-798 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+24 -128 moveto
+798 0 rlineto
+0 -20 rlineto
+-798 0 rlineto
+closepath
+stroke
+isolatin1encoding /_Helvetica /Helvetica RE
+/_Helvetica findfont
+10 scalefont setfont
+28 -141 moveto
+(libakonadi) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+24 -148 moveto
+847 0 rlineto
+0 -203 rlineto
+-847 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+24 -148 moveto
+847 0 rlineto
+0 -203 rlineto
+-847 0 rlineto
+closepath
+stroke
+isolatin1encoding /_TimesRoman /TimesRoman RE
+/_TimesRoman findfont
+10 scalefont setfont
+1.0 1.0 1.0 setrgbcolor
+newpath
+48 -440 moveto
+81 0 rlineto
+0 -21 rlineto
+-81 0 rlineto
+closepath
+eofill
+newpath
+48 -440 moveto
+81 0 rlineto
+0 -1 rlineto
+-81 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+48 -440 moveto
+82 0 rlineto
+0 -2 rlineto
+-82 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+48 -442 moveto
+81 0 rlineto
+0 -19 rlineto
+-81 0 rlineto
+closepath
+eofill
+isolatin1encoding /_Helvetica /Helvetica RE
+/_Helvetica findfont
+10 scalefont setfont
+0.0 0.0 0.0 setrgbcolor
+52 -455 moveto
+(VCardResource) show
+newpath
+48 -440 moveto
+82 0 rlineto
+0 -22 rlineto
+-82 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+152 -440 moveto
+70 0 rlineto
+0 -21 rlineto
+-70 0 rlineto
+closepath
+eofill
+newpath
+152 -440 moveto
+70 0 rlineto
+0 -1 rlineto
+-70 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+152 -440 moveto
+71 0 rlineto
+0 -2 rlineto
+-71 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+152 -442 moveto
+70 0 rlineto
+0 -19 rlineto
+-70 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+156 -455 moveto
+(ICalResource) show
+newpath
+152 -440 moveto
+71 0 rlineto
+0 -22 rlineto
+-71 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+96 -288 moveto
+99 0 rlineto
+0 -36 rlineto
+-99 0 rlineto
+closepath
+eofill
+newpath
+96 -288 moveto
+99 0 rlineto
+0 -14 rlineto
+-99 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+96 -288 moveto
+100 0 rlineto
+0 -15 rlineto
+-100 0 rlineto
+closepath
+stroke
+99 -302 moveto
+(<<KApplication>>) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+96 -303 moveto
+99 0 rlineto
+0 -19 rlineto
+-99 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+112 -316 moveto
+(ResourceBase) show
+newpath
+96 -288 moveto
+100 0 rlineto
+0 -37 rlineto
+-100 0 rlineto
+closepath
+stroke
+1.0 1.0 0.78431374 setrgbcolor
+newpath
+248 -288 moveto
+59 0 rlineto
+0 -21 rlineto
+-59 0 rlineto
+closepath
+eofill
+newpath
+248 -288 moveto
+59 0 rlineto
+0 -1 rlineto
+-59 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+248 -288 moveto
+60 0 rlineto
+0 -2 rlineto
+-60 0 rlineto
+closepath
+stroke
+1.0 1.0 0.78431374 setrgbcolor
+newpath
+248 -290 moveto
+59 0 rlineto
+0 -19 rlineto
+-59 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+259 -303 moveto
+(Monitor) show
+newpath
+248 -288 moveto
+60 0 rlineto
+0 -22 rlineto
+-60 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+432 -280 moveto
+123 0 rlineto
+0 -36 rlineto
+-123 0 rlineto
+closepath
+eofill
+newpath
+432 -280 moveto
+123 0 rlineto
+0 -14 rlineto
+-123 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+432 -280 moveto
+124 0 rlineto
+0 -15 rlineto
+-124 0 rlineto
+closepath
+stroke
+435 -294 moveto
+(<<QCoreApplication>>) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+432 -295 moveto
+123 0 rlineto
+0 -19 rlineto
+-123 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+446 -308 moveto
+(SearchProviderBase) show
+newpath
+432 -280 moveto
+124 0 rlineto
+0 -37 rlineto
+-124 0 rlineto
+closepath
+stroke
+[5.0 5.0 ] 0 setdash
+newpath
+256 -310 moveto
+256 -640 lineto
+stroke
+[] 0 setdash
+newpath
+249 -628 moveto
+256 -640 lineto
+stroke
+newpath
+263 -628 moveto
+256 -640 lineto
+stroke
+[5.0 5.0 ] 0 setdash
+newpath
+144 -576 moveto
+144 -325 lineto
+stroke
+[] 0 setdash
+newpath
+151 -337 moveto
+144 -325 lineto
+stroke
+newpath
+137 -337 moveto
+144 -325 lineto
+stroke
+183 -427 moveto
+(create) show
+newpath
+114 -440 moveto
+114 -325 lineto
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+114 -325 moveto
+121 -337 lineto
+107 -337 lineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+114 -325 moveto
+121 -337 lineto
+107 -337 lineto
+closepath
+stroke
+newpath
+168 -440 moveto
+168 -325 lineto
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+168 -325 moveto
+175 -337 lineto
+161 -337 lineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+168 -325 moveto
+175 -337 lineto
+161 -337 lineto
+closepath
+stroke
+[5.0 5.0 ] 0 setdash
+newpath
+548 -576 moveto
+548 -317 lineto
+stroke
+[] 0 setdash
+newpath
+555 -329 moveto
+548 -317 lineto
+stroke
+newpath
+541 -329 moveto
+548 -317 lineto
+stroke
+573 -453 moveto
+(create) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+312 -448 moveto
+122 0 rlineto
+0 -21 rlineto
+-122 0 rlineto
+closepath
+eofill
+newpath
+312 -448 moveto
+122 0 rlineto
+0 -1 rlineto
+-122 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+312 -448 moveto
+123 0 rlineto
+0 -2 rlineto
+-123 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+312 -450 moveto
+122 0 rlineto
+0 -19 rlineto
+-122 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+316 -463 moveto
+(MessageSearchProvider) show
+newpath
+312 -448 moveto
+123 0 rlineto
+0 -22 rlineto
+-123 0 rlineto
+closepath
+stroke
+newpath
+432 -448 moveto
+432 -317 lineto
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+432 -317 moveto
+439 -329 lineto
+425 -329 lineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+432 -317 moveto
+439 -329 lineto
+425 -329 lineto
+closepath
+stroke
+[5.0 5.0 ] 0 setdash
+newpath
+576 -877 moveto
+576 -1032 lineto
+stroke
+[] 0 setdash
+newpath
+569 -1020 moveto
+576 -1032 lineto
+stroke
+newpath
+583 -1020 moveto
+576 -1032 lineto
+stroke
+601 -960 moveto
+(clear old values) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+624 -176 moveto
+59 0 rlineto
+0 -21 rlineto
+-59 0 rlineto
+closepath
+eofill
+newpath
+624 -176 moveto
+59 0 rlineto
+0 -1 rlineto
+-59 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+624 -176 moveto
+60 0 rlineto
+0 -2 rlineto
+-60 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+624 -178 moveto
+59 0 rlineto
+0 -19 rlineto
+-59 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+646 -191 moveto
+(Job) show
+newpath
+624 -176 moveto
+60 0 rlineto
+0 -22 rlineto
+-60 0 rlineto
+closepath
+stroke
+[5.0 5.0 ] 0 setdash
+newpath
+308 -288 moveto
+624 -198 lineto
+stroke
+[] 0 setdash
+newpath
+614 -208 moveto
+624 -198 lineto
+stroke
+newpath
+610 -194 moveto
+624 -198 lineto
+stroke
+[5.0 5.0 ] 0 setdash
+newpath
+656 -198 moveto
+656 -640 lineto
+stroke
+[] 0 setdash
+newpath
+649 -628 moveto
+656 -640 lineto
+stroke
+newpath
+663 -628 moveto
+656 -640 lineto
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+752 -240 moveto
+76 0 rlineto
+0 -21 rlineto
+-76 0 rlineto
+closepath
+eofill
+newpath
+752 -240 moveto
+76 0 rlineto
+0 -1 rlineto
+-76 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+752 -240 moveto
+77 0 rlineto
+0 -2 rlineto
+-77 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+752 -242 moveto
+76 0 rlineto
+0 -19 rlineto
+-76 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+756 -255 moveto
+(ItemDeleteJob) show
+newpath
+752 -240 moveto
+77 0 rlineto
+0 -22 rlineto
+-77 0 rlineto
+closepath
+stroke
+newpath
+752 -240 moveto
+684 -198 lineto
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+684 -198 moveto
+697 -198 lineto
+690 -210 lineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+684 -198 moveto
+697 -198 lineto
+690 -210 lineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+752 -272 moveto
+71 0 rlineto
+0 -21 rlineto
+-71 0 rlineto
+closepath
+eofill
+newpath
+752 -272 moveto
+71 0 rlineto
+0 -1 rlineto
+-71 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+752 -272 moveto
+72 0 rlineto
+0 -2 rlineto
+-72 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+752 -274 moveto
+71 0 rlineto
+0 -19 rlineto
+-71 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+783 -287 moveto
+(...) show
+newpath
+752 -272 moveto
+72 0 rlineto
+0 -22 rlineto
+-72 0 rlineto
+closepath
+stroke
+newpath
+752 -272 moveto
+684 -198 lineto
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+684 -198 moveto
+697 -202 lineto
+686 -211 lineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+684 -198 moveto
+697 -202 lineto
+686 -211 lineto
+closepath
+stroke
+[5.0 5.0 ] 0 setdash
+newpath
+316 -782 moveto
+408 -944 lineto
+stroke
+[] 0 setdash
+newpath
+395 -937 moveto
+408 -944 lineto
+stroke
+newpath
+408 -930 moveto
+408 -944 lineto
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+24 -16 moveto
+198 0 rlineto
+0 -20 rlineto
+-198 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+24 -16 moveto
+198 0 rlineto
+0 -20 rlineto
+-198 0 rlineto
+closepath
+stroke
+28 -29 moveto
+(kabc) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+24 -36 moveto
+247 0 rlineto
+0 -79 rlineto
+-247 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+24 -36 moveto
+247 0 rlineto
+0 -79 rlineto
+-247 0 rlineto
+closepath
+stroke
+isolatin1encoding /_TimesRoman /TimesRoman RE
+/_TimesRoman findfont
+10 scalefont setfont
+1.0 1.0 1.0 setrgbcolor
+newpath
+304 -16 moveto
+90 0 rlineto
+0 -20 rlineto
+-90 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+304 -16 moveto
+90 0 rlineto
+0 -20 rlineto
+-90 0 rlineto
+closepath
+stroke
+isolatin1encoding /_Helvetica /Helvetica RE
+/_Helvetica findfont
+10 scalefont setfont
+308 -29 moveto
+(kmime) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+304 -36 moveto
+139 0 rlineto
+0 -79 rlineto
+-139 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+304 -36 moveto
+139 0 rlineto
+0 -79 rlineto
+-139 0 rlineto
+closepath
+stroke
+isolatin1encoding /_TimesRoman /TimesRoman RE
+/_TimesRoman findfont
+10 scalefont setfont
+1.0 1.0 1.0 setrgbcolor
+newpath
+480 -16 moveto
+90 0 rlineto
+0 -20 rlineto
+-90 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+480 -16 moveto
+90 0 rlineto
+0 -20 rlineto
+-90 0 rlineto
+closepath
+stroke
+isolatin1encoding /_Helvetica /Helvetica RE
+/_Helvetica findfont
+10 scalefont setfont
+484 -29 moveto
+(kioslave) show
+1.0 1.0 1.0 setrgbcolor
+newpath
+480 -36 moveto
+139 0 rlineto
+0 -79 rlineto
+-139 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+480 -36 moveto
+139 0 rlineto
+0 -79 rlineto
+-139 0 rlineto
+closepath
+stroke
+isolatin1encoding /_TimesRoman /TimesRoman RE
+/_TimesRoman findfont
+10 scalefont setfont
+1.0 1.0 1.0 setrgbcolor
+newpath
+40 -48 moveto
+239 -48 lineto
+249 -58 lineto
+249 -105 lineto
+40 -105 lineto
+40 -48 lineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+40 -48 moveto
+239 -48 lineto
+249 -58 lineto
+249 -105 lineto
+40 -105 lineto
+40 -48 lineto
+stroke
+0.69803923 0.69803923 0.69803923 setrgbcolor
+newpath
+239 -48 moveto
+249 -58 lineto
+239 -58 lineto
+239 -48 lineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+239 -48 moveto
+249 -58 lineto
+239 -58 lineto
+239 -48 lineto
+stroke
+isolatin1encoding /_Helvetica /Helvetica RE
+/_Helvetica findfont
+10 scalefont setfont
+42 -63 moveto
+(some widgets to manipulate addressees) show
+42 -76 moveto
+(using Jobs and Monitor) show
+[5.0 5.0 ] 0 setdash
+newpath
+126 -48 moveto
+126 -16 lineto
+stroke
+newpath
+264 -116 moveto
+264 -288 lineto
+stroke
+[] 0 setdash
+newpath
+257 -276 moveto
+264 -288 lineto
+stroke
+newpath
+271 -276 moveto
+264 -288 lineto
+stroke
+[5.0 5.0 ] 0 setdash
+newpath
+272 -116 moveto
+624 -176 lineto
+stroke
+[] 0 setdash
+newpath
+610 -180 moveto
+624 -176 lineto
+stroke
+newpath
+613 -167 moveto
+624 -176 lineto
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+328 -64 moveto
+80 0 rlineto
+0 -21 rlineto
+-80 0 rlineto
+closepath
+eofill
+newpath
+328 -64 moveto
+80 0 rlineto
+0 -1 rlineto
+-80 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+328 -64 moveto
+81 0 rlineto
+0 -2 rlineto
+-81 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+328 -66 moveto
+80 0 rlineto
+0 -19 rlineto
+-80 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+332 -79 moveto
+(MessageModel) show
+newpath
+328 -64 moveto
+81 0 rlineto
+0 -22 rlineto
+-81 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+512 -64 moveto
+72 0 rlineto
+0 -21 rlineto
+-72 0 rlineto
+closepath
+eofill
+newpath
+512 -64 moveto
+72 0 rlineto
+0 -1 rlineto
+-72 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+512 -64 moveto
+73 0 rlineto
+0 -2 rlineto
+-73 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+512 -66 moveto
+72 0 rlineto
+0 -19 rlineto
+-72 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+516 -79 moveto
+(AkonadiSlave) show
+newpath
+512 -64 moveto
+73 0 rlineto
+0 -22 rlineto
+-73 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+448 -440 moveto
+72 0 rlineto
+0 -21 rlineto
+-72 0 rlineto
+closepath
+eofill
+newpath
+448 -440 moveto
+72 0 rlineto
+0 -1 rlineto
+-72 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+448 -440 moveto
+73 0 rlineto
+0 -2 rlineto
+-73 0 rlineto
+closepath
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+448 -442 moveto
+72 0 rlineto
+0 -19 rlineto
+-72 0 rlineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+452 -455 moveto
+(StrigiProvider) show
+newpath
+448 -440 moveto
+73 0 rlineto
+0 -22 rlineto
+-73 0 rlineto
+closepath
+stroke
+newpath
+494 -440 moveto
+494 -317 lineto
+stroke
+1.0 1.0 1.0 setrgbcolor
+newpath
+494 -317 moveto
+501 -329 lineto
+487 -329 lineto
+closepath
+eofill
+0.0 0.0 0.0 setrgbcolor
+newpath
+494 -317 moveto
+501 -329 lineto
+487 -329 lineto
+closepath
+stroke
+showpage
+%%Trailer
diff --git a/doc/pics/akonadi_overview_uml_small.png b/doc/pics/akonadi_overview_uml_small.png
new file mode 100644 (file)
index 0000000..06112b1
Binary files /dev/null and b/doc/pics/akonadi_overview_uml_small.png differ
diff --git a/doc/pics/concept.eps b/doc/pics/concept.eps
new file mode 100644 (file)
index 0000000..f2a8913
--- /dev/null
@@ -0,0 +1,2351 @@
+%!PS-Adobe-3.0 EPSF-3.0
+%%For: 
+%%Title: 
+%%Creator: Scribus1.3.4
+%%Pages: 1
+%%BoundingBox: 0 0 595 420
+%%HiResBoundingBox: 0 0 595.28 419.53
+%%LanguageLevel: 3
+%%EndComments
+%%BeginProlog
+/Scribusdict 100 dict def
+Scribusdict begin
+/sp {showpage} bind def
+/oldsetgray /setgray load def
+/cmyk {setcmykcolor} def
+/m {moveto} bind def
+/l {lineto} bind def
+/li {lineto} bind def
+/cu {curveto} bind def
+/cl {closepath} bind def
+/gs {gsave} bind def
+/gr {grestore} bind def
+/tr {translate} bind def
+/ro {rotate} bind def
+/sh {show} bind def
+/shg {setcmykcolor moveto glyphshow} def
+/shgsp {moveto glyphshow} def
+/sc {scale} bind def
+/se {selectfont} bind def
+/sf {setfont} bind def
+/sw {setlinewidth} bind def
+/f  {findfont} bind def
+/fi {fill} bind def
+/st {stroke} bind def
+/shgf {gs dup scale begin cvx exec fill end gr} bind def
+/shgs {gs dup 1 exch div currentlinewidth mul sw dup scale
+       begin cvx exec st end gr} bind def
+/bEPS {
+    /b4_Inc_state save def
+    /dict_count countdictstack def
+    /op_count count 1 sub def
+    userdict begin
+    /showpage { } def
+    0 setgray 0 setlinecap
+    1 setlinewidth 0 setlinejoin
+    10 setmiterlimit [ ] 0 setdash newpath
+    /languagelevel where
+    {pop languagelevel
+    1 ne
+    {false setstrokeadjust false setoverprint
+    } if } if } bind def
+/eEPS { count op_count sub {pop} repeat
+    countdictstack dict_count sub {end} repeat
+    b4_Inc_state restore } bind def
+    end
+%%EndProlog
+%%BeginSetup
+/pdfmark where {pop} {userdict /pdfmark /cleartomark load put} ifelse
+/ArialMT 37 dict def
+ArialMT begin
+/G11 { newpath
+2.33891 -12.1045 m
+2.33891 -12.1045 1.61141 -11.1866 1.10844 -9.95609 cu
+1.10844 -9.95609 0.605469 -8.72563 0.605469 -7.40719 cu
+0.605469 -7.40719 0.605469 -6.24516 0.981406 -5.18063 cu
+0.981406 -5.18063 1.42094 -3.94531 2.33891 -2.71969 cu
+2.33891 -2.71969 2.96875 -2.71969 2.96875 -2.71969 cu
+2.96875 -2.71969 2.37797 -3.73531 2.1875 -4.16984 cu
+2.1875 -4.16984 1.88969 -4.84375 1.71875 -5.57609 cu
+1.71875 -5.57609 1.50875 -6.48922 1.50875 -7.41203 cu
+1.50875 -7.41203 1.50875 -9.76078 2.96875 -12.1045 cu
+2.96875 -12.1045 2.33891 -12.1045 2.33891 -12.1045 cu
+cl
+} bind def
+/G12 { newpath
+1.23531 -12.1045 m
+1.23531 -12.1045 0.605469 -12.1045 0.605469 -12.1045 cu
+0.605469 -12.1045 2.06547 -9.76078 2.06547 -7.41203 cu
+2.06547 -7.41203 2.06547 -6.49406 1.85547 -5.59078 cu
+1.85547 -5.59078 1.68953 -4.85844 1.39156 -4.18453 cu
+1.39156 -4.18453 1.20125 -3.74516 0.605469 -2.71969 cu
+0.605469 -2.71969 1.23531 -2.71969 1.23531 -2.71969 cu
+1.23531 -2.71969 2.15328 -3.94531 2.59281 -5.18063 cu
+2.59281 -5.18063 2.96875 -6.24516 2.96875 -7.40719 cu
+2.96875 -7.40719 2.96875 -8.72563 2.46328 -9.95609 cu
+2.46328 -9.95609 1.95797 -11.1866 1.23531 -12.1045 cu
+cl
+} bind def
+/G16 { newpath
+0.317344 -7.85156 m
+0.317344 -7.85156 0.317344 -6.96781 0.317344 -6.96781 cu
+0.317344 -6.96781 3.01766 -6.96781 3.01766 -6.96781 cu
+3.01766 -6.96781 3.01766 -7.85156 3.01766 -7.85156 cu
+3.01766 -7.85156 0.317344 -7.85156 0.317344 -7.85156 cu
+cl
+} bind def
+/G18 { newpath
+0 -10.122 m
+0 -10.122 2.07516 -2.71969 2.07516 -2.71969 cu
+2.07516 -2.71969 2.77828 -2.71969 2.77828 -2.71969 cu
+2.77828 -2.71969 0.707969 -10.122 0.707969 -10.122 cu
+0.707969 -10.122 0 -10.122 0 -10.122 cu
+cl
+} bind def
+/G20 { newpath
+3.72562 -10 m
+3.72562 -10 2.84672 -10 2.84672 -10 cu
+2.84672 -10 2.84672 -4.39938 2.84672 -4.39938 cu
+2.84672 -4.39938 2.52937 -4.70219 2.01422 -5.005 cu
+2.01422 -5.005 1.49906 -5.30766 1.08891 -5.45891 cu
+1.08891 -5.45891 1.08891 -4.60938 1.08891 -4.60938 cu
+1.08891 -4.60938 1.82625 -4.26266 2.37797 -3.76953 cu
+2.37797 -3.76953 2.92969 -3.27641 3.15922 -2.8125 cu
+3.15922 -2.8125 3.72562 -2.8125 3.72562 -2.8125 cu
+3.72562 -2.8125 3.72562 -10 3.72562 -10 cu
+cl
+} bind def
+/G21 { newpath
+5.03422 -9.15531 m
+5.03422 -9.15531 5.03422 -10 5.03422 -10 cu
+5.03422 -10 0.302812 -10 0.302812 -10 cu
+0.302812 -10 0.292969 -9.68266 0.405313 -9.38969 cu
+0.405313 -9.38969 0.585938 -8.90625 0.983906 -8.4375 cu
+0.983906 -8.4375 1.38187 -7.96875 2.13375 -7.35344 cu
+2.13375 -7.35344 3.30078 -6.39641 3.71094 -5.83734 cu
+3.71094 -5.83734 4.12109 -5.27828 4.12109 -4.78031 cu
+4.12109 -4.78031 4.12109 -4.25781 3.7475 -3.89891 cu
+3.7475 -3.89891 3.37406 -3.54 2.77344 -3.54 cu
+2.77344 -3.54 2.13875 -3.54 1.75781 -3.92094 cu
+1.75781 -3.92094 1.37703 -4.30172 1.37203 -4.97562 cu
+1.37203 -4.97562 0.46875 -4.88281 0.46875 -4.88281 cu
+0.46875 -4.88281 0.561562 -3.87203 1.16703 -3.34234 cu
+1.16703 -3.34234 1.7725 -2.8125 2.79297 -2.8125 cu
+2.79297 -2.8125 3.82328 -2.8125 4.42375 -3.38375 cu
+4.42375 -3.38375 5.02438 -3.955 5.02438 -4.79984 cu
+5.02438 -4.79984 5.02438 -5.22953 4.84859 -5.64453 cu
+4.84859 -5.64453 4.67281 -6.05953 4.26516 -6.51859 cu
+4.26516 -6.51859 3.8575 -6.9775 2.91016 -7.77828 cu
+2.91016 -7.77828 2.11922 -8.44234 1.89453 -8.67922 cu
+1.89453 -8.67922 1.67 -8.91594 1.52344 -9.15531 cu
+1.52344 -9.15531 5.03422 -9.15531 5.03422 -9.15531 cu
+cl
+} bind def
+/G36 { newpath
+-0.0146875 -10 m
+-0.0146875 -10 2.73438 -2.84172 2.73438 -2.84172 cu
+2.73438 -2.84172 3.75484 -2.84172 3.75484 -2.84172 cu
+3.75484 -2.84172 6.68453 -10 6.68453 -10 cu
+6.68453 -10 5.60547 -10 5.60547 -10 cu
+5.60547 -10 4.77047 -7.83203 4.77047 -7.83203 cu
+4.77047 -7.83203 1.77734 -7.83203 1.77734 -7.83203 cu
+1.77734 -7.83203 0.99125 -10 0.99125 -10 cu
+0.99125 -10 -0.0146875 -10 -0.0146875 -10 cu
+-0.0146875 -10 -0.0146875 -10 -0.0146875 -10 cu
+cl
+2.05078 -7.06047 m
+2.05078 -7.06047 4.4775 -7.06047 4.4775 -7.06047 cu
+4.4775 -7.06047 3.73047 -5.07812 3.73047 -5.07812 cu
+3.73047 -5.07812 3.38875 -4.17484 3.22266 -3.59375 cu
+3.22266 -3.59375 3.08594 -4.28219 2.83688 -4.96094 cu
+2.83688 -4.96094 2.05078 -7.06047 2.05078 -7.06047 cu
+cl
+} bind def
+/G37 { newpath
+0.7325 -10 m
+0.7325 -10 0.7325 -2.84172 0.7325 -2.84172 cu
+0.7325 -2.84172 3.41797 -2.84172 3.41797 -2.84172 cu
+3.41797 -2.84172 4.23828 -2.84172 4.73391 -3.05906 cu
+4.73391 -3.05906 5.22953 -3.27641 5.51031 -3.72812 cu
+5.51031 -3.72812 5.79109 -4.17969 5.79109 -4.67281 cu
+5.79109 -4.67281 5.79109 -5.13187 5.54203 -5.53719 cu
+5.54203 -5.53719 5.29297 -5.94234 4.79 -6.19141 cu
+4.79 -6.19141 5.43953 -6.38187 5.78859 -6.84094 cu
+5.78859 -6.84094 6.13766 -7.29984 6.13766 -7.92484 cu
+6.13766 -7.92484 6.13766 -8.42766 5.92531 -8.85984 cu
+5.92531 -8.85984 5.71297 -9.29203 5.40047 -9.52641 cu
+5.40047 -9.52641 5.08797 -9.76078 4.61672 -9.88047 cu
+4.61672 -9.88047 4.14547 -10 3.46188 -10 cu
+3.46188 -10 0.7325 -10 0.7325 -10 cu
+0.7325 -10 0.7325 -10 0.7325 -10 cu
+cl
+1.67969 -5.84953 m
+1.67969 -5.84953 3.2275 -5.84953 3.2275 -5.84953 cu
+3.2275 -5.84953 3.8575 -5.84953 4.13094 -5.76656 cu
+4.13094 -5.76656 4.49219 -5.65922 4.67531 -5.41016 cu
+4.67531 -5.41016 4.85844 -5.16109 4.85844 -4.78516 cu
+4.85844 -4.78516 4.85844 -4.42875 4.6875 -4.15781 cu
+4.6875 -4.15781 4.51656 -3.88672 4.19922 -3.78672 cu
+4.19922 -3.78672 3.88187 -3.68656 3.11031 -3.68656 cu
+3.11031 -3.68656 1.67969 -3.68656 1.67969 -3.68656 cu
+1.67969 -3.68656 1.67969 -5.84953 1.67969 -5.84953 cu
+1.67969 -5.84953 1.67969 -5.84953 1.67969 -5.84953 cu
+cl
+1.67969 -9.15531 m
+1.67969 -9.15531 3.46188 -9.15531 3.46188 -9.15531 cu
+3.46188 -9.15531 3.92094 -9.15531 4.10641 -9.12109 cu
+4.10641 -9.12109 4.43359 -9.0625 4.65328 -8.92578 cu
+4.65328 -8.92578 4.87312 -8.78906 5.01469 -8.52781 cu
+5.01469 -8.52781 5.15625 -8.26656 5.15625 -7.92484 cu
+5.15625 -7.92484 5.15625 -7.52438 4.95109 -7.22906 cu
+4.95109 -7.22906 4.74609 -6.93359 4.38234 -6.81406 cu
+4.38234 -6.81406 4.01859 -6.69437 3.335 -6.69437 cu
+3.335 -6.69437 1.67969 -6.69437 1.67969 -6.69437 cu
+1.67969 -6.69437 1.67969 -9.15531 1.67969 -9.15531 cu
+cl
+} bind def
+/G38 { newpath
+5.87891 -7.49016 m
+5.87891 -7.49016 6.82625 -7.72953 6.82625 -7.72953 cu
+6.82625 -7.72953 6.52828 -8.89641 5.75438 -9.50922 cu
+5.75438 -9.50922 4.98047 -10.122 3.86234 -10.122 cu
+3.86234 -10.122 2.70516 -10.122 1.98 -9.65094 cu
+1.98 -9.65094 1.25484 -9.17969 0.876406 -8.28609 cu
+0.876406 -8.28609 0.498125 -7.3925 0.498125 -6.36719 cu
+0.498125 -6.36719 0.498125 -5.24906 0.925312 -4.41656 cu
+0.925312 -4.41656 1.3525 -3.58391 2.14109 -3.15187 cu
+2.14109 -3.15187 2.92969 -2.71969 3.87703 -2.71969 cu
+3.87703 -2.71969 4.95125 -2.71969 5.68359 -3.26656 cu
+5.68359 -3.26656 6.41609 -3.81344 6.70406 -4.80469 cu
+6.70406 -4.80469 5.77156 -5.02438 5.77156 -5.02438 cu
+5.77156 -5.02438 5.5225 -4.24313 5.04875 -3.88672 cu
+5.04875 -3.88672 4.57516 -3.53031 3.8575 -3.53031 cu
+3.8575 -3.53031 3.03219 -3.53031 2.47797 -3.92578 cu
+2.47797 -3.92578 1.92391 -4.32125 1.69922 -4.98781 cu
+1.69922 -4.98781 1.47469 -5.65422 1.47469 -6.36234 cu
+1.47469 -6.36234 1.47469 -7.27531 1.74078 -7.95656 cu
+1.74078 -7.95656 2.00687 -8.63766 2.56828 -8.97469 cu
+2.56828 -8.97469 3.12984 -9.31156 3.78422 -9.31156 cu
+3.78422 -9.31156 4.58016 -9.31156 5.13187 -8.85266 cu
+5.13187 -8.85266 5.68359 -8.39359 5.87891 -7.49016 cu
+cl
+} bind def
+/G39 { newpath
+0.771563 -10 m
+0.771563 -10 0.771563 -2.84172 0.771563 -2.84172 cu
+0.771563 -2.84172 3.23734 -2.84172 3.23734 -2.84172 cu
+3.23734 -2.84172 4.07234 -2.84172 4.51172 -2.94437 cu
+4.51172 -2.94437 5.12703 -3.08594 5.56156 -3.45703 cu
+5.56156 -3.45703 6.12797 -3.93547 6.40875 -4.68016 cu
+6.40875 -4.68016 6.68953 -5.42484 6.68953 -6.38187 cu
+6.68953 -6.38187 6.68953 -7.19719 6.49906 -7.82719 cu
+6.49906 -7.82719 6.30859 -8.45703 6.01078 -8.86969 cu
+6.01078 -8.86969 5.71297 -9.28219 5.35891 -9.51906 cu
+5.35891 -9.51906 5.00484 -9.75578 4.50438 -9.87797 cu
+4.50438 -9.87797 4.00391 -10 3.35453 -10 cu
+3.35453 -10 0.771563 -10 0.771563 -10 cu
+0.771563 -10 0.771563 -10 0.771563 -10 cu
+cl
+1.71875 -9.15531 m
+1.71875 -9.15531 3.24703 -9.15531 3.24703 -9.15531 cu
+3.24703 -9.15531 3.95516 -9.15531 4.35797 -9.02344 cu
+4.35797 -9.02344 4.76078 -8.89156 5 -8.65234 cu
+5 -8.65234 5.33688 -8.31547 5.52484 -7.74656 cu
+5.52484 -7.74656 5.71297 -7.17766 5.71297 -6.36719 cu
+5.71297 -6.36719 5.71297 -5.24406 5.34422 -4.64109 cu
+5.34422 -4.64109 4.97562 -4.03812 4.44828 -3.83297 cu
+4.44828 -3.83297 4.06734 -3.68656 3.22266 -3.68656 cu
+3.22266 -3.68656 1.71875 -3.68656 1.71875 -3.68656 cu
+1.71875 -3.68656 1.71875 -9.15531 1.71875 -9.15531 cu
+cl
+} bind def
+/G44 { newpath
+0.932656 -10 m
+0.932656 -10 0.932656 -2.84172 0.932656 -2.84172 cu
+0.932656 -2.84172 1.87984 -2.84172 1.87984 -2.84172 cu
+1.87984 -2.84172 1.87984 -10 1.87984 -10 cu
+1.87984 -10 0.932656 -10 0.932656 -10 cu
+cl
+} bind def
+/G48 { newpath
+0.742188 -10 m
+0.742188 -10 0.742188 -2.84172 0.742188 -2.84172 cu
+0.742188 -2.84172 2.16797 -2.84172 2.16797 -2.84172 cu
+2.16797 -2.84172 3.86234 -7.91016 3.86234 -7.91016 cu
+3.86234 -7.91016 4.09672 -8.61812 4.20406 -8.96969 cu
+4.20406 -8.96969 4.32625 -8.57906 4.585 -7.82219 cu
+4.585 -7.82219 6.29891 -2.84172 6.29891 -2.84172 cu
+6.29891 -2.84172 7.57328 -2.84172 7.57328 -2.84172 cu
+7.57328 -2.84172 7.57328 -10 7.57328 -10 cu
+7.57328 -10 6.66016 -10 6.66016 -10 cu
+6.66016 -10 6.66016 -4.00875 6.66016 -4.00875 cu
+6.66016 -4.00875 4.58016 -10 4.58016 -10 cu
+4.58016 -10 3.72562 -10 3.72562 -10 cu
+3.72562 -10 1.65531 -3.90625 1.65531 -3.90625 cu
+1.65531 -3.90625 1.65531 -10 1.65531 -10 cu
+1.65531 -10 0.742188 -10 0.742188 -10 cu
+cl
+} bind def
+/G49 { newpath
+0.761719 -10 m
+0.761719 -10 0.761719 -2.84172 0.761719 -2.84172 cu
+0.761719 -2.84172 1.73344 -2.84172 1.73344 -2.84172 cu
+1.73344 -2.84172 5.49313 -8.46187 5.49313 -8.46187 cu
+5.49313 -8.46187 5.49313 -2.84172 5.49313 -2.84172 cu
+5.49313 -2.84172 6.40141 -2.84172 6.40141 -2.84172 cu
+6.40141 -2.84172 6.40141 -10 6.40141 -10 cu
+6.40141 -10 5.42969 -10 5.42969 -10 cu
+5.42969 -10 1.67 -4.375 1.67 -4.375 cu
+1.67 -4.375 1.67 -10 1.67 -10 cu
+1.67 -10 0.761719 -10 0.761719 -10 cu
+cl
+} bind def
+/G51 { newpath
+0.771563 -10 m
+0.771563 -10 0.771563 -2.84172 0.771563 -2.84172 cu
+0.771563 -2.84172 3.47172 -2.84172 3.47172 -2.84172 cu
+3.47172 -2.84172 4.18453 -2.84172 4.56062 -2.91016 cu
+4.56062 -2.91016 5.08797 -2.99797 5.44437 -3.24469 cu
+5.44437 -3.24469 5.80078 -3.49125 6.01797 -3.93562 cu
+6.01797 -3.93562 6.23531 -4.37984 6.23531 -4.91203 cu
+6.23531 -4.91203 6.23531 -5.82516 5.65422 -6.4575 cu
+5.65422 -6.4575 5.07328 -7.08984 3.55469 -7.08984 cu
+3.55469 -7.08984 1.71875 -7.08984 1.71875 -7.08984 cu
+1.71875 -7.08984 1.71875 -10 1.71875 -10 cu
+1.71875 -10 0.771563 -10 0.771563 -10 cu
+0.771563 -10 0.771563 -10 0.771563 -10 cu
+cl
+1.71875 -6.24516 m
+1.71875 -6.24516 3.56937 -6.24516 3.56937 -6.24516 cu
+3.56937 -6.24516 4.48734 -6.24516 4.87297 -5.90344 cu
+4.87297 -5.90344 5.25875 -5.56156 5.25875 -4.94141 cu
+5.25875 -4.94141 5.25875 -4.49219 5.03172 -4.17234 cu
+5.03172 -4.17234 4.80469 -3.8525 4.43359 -3.75 cu
+4.43359 -3.75 4.19437 -3.68656 3.54984 -3.68656 cu
+3.54984 -3.68656 1.71875 -3.68656 1.71875 -3.68656 cu
+1.71875 -3.68656 1.71875 -6.24516 1.71875 -6.24516 cu
+cl
+} bind def
+/G53 { newpath
+0.786094 -10 m
+0.786094 -10 0.786094 -2.84172 0.786094 -2.84172 cu
+0.786094 -2.84172 3.96 -2.84172 3.96 -2.84172 cu
+3.96 -2.84172 4.91703 -2.84172 5.415 -3.03469 cu
+5.415 -3.03469 5.91312 -3.2275 6.21094 -3.71578 cu
+6.21094 -3.71578 6.50875 -4.20406 6.50875 -4.79484 cu
+6.50875 -4.79484 6.50875 -5.55656 6.01562 -6.07906 cu
+6.01562 -6.07906 5.5225 -6.60156 4.49219 -6.74313 cu
+4.49219 -6.74313 4.86813 -6.92375 5.06344 -7.09953 cu
+5.06344 -7.09953 5.47859 -7.48047 5.84969 -8.05172 cu
+5.84969 -8.05172 7.09469 -10 7.09469 -10 cu
+7.09469 -10 5.90328 -10 5.90328 -10 cu
+5.90328 -10 4.95609 -8.51078 4.95609 -8.51078 cu
+4.95609 -8.51078 4.54109 -7.86625 4.2725 -7.52453 cu
+4.2725 -7.52453 4.00391 -7.18266 3.79141 -7.04594 cu
+3.79141 -7.04594 3.57906 -6.90922 3.35938 -6.85547 cu
+3.35938 -6.85547 3.19828 -6.82125 2.83203 -6.82125 cu
+2.83203 -6.82125 1.73344 -6.82125 1.73344 -6.82125 cu
+1.73344 -6.82125 1.73344 -10 1.73344 -10 cu
+1.73344 -10 0.786094 -10 0.786094 -10 cu
+0.786094 -10 0.786094 -10 0.786094 -10 cu
+cl
+1.73344 -6.00094 m
+1.73344 -6.00094 3.76953 -6.00094 3.76953 -6.00094 cu
+3.76953 -6.00094 4.41891 -6.00094 4.78516 -5.86672 cu
+4.78516 -5.86672 5.15141 -5.73234 5.34172 -5.43703 cu
+5.34172 -5.43703 5.53219 -5.14156 5.53219 -4.79484 cu
+5.53219 -4.79484 5.53219 -4.28703 5.16359 -3.96 cu
+5.16359 -3.96 4.795 -3.63281 3.99906 -3.63281 cu
+3.99906 -3.63281 1.73344 -3.63281 1.73344 -3.63281 cu
+1.73344 -3.63281 1.73344 -6.00094 1.73344 -6.00094 cu
+cl
+} bind def
+/G54 { newpath
+0.449219 -7.70016 m
+0.449219 -7.70016 1.34281 -7.62203 1.34281 -7.62203 cu
+1.34281 -7.62203 1.40625 -8.15922 1.63813 -8.50344 cu
+1.63813 -8.50344 1.87016 -8.84766 2.35844 -9.06016 cu
+2.35844 -9.06016 2.84672 -9.2725 3.45703 -9.2725 cu
+3.45703 -9.2725 3.99906 -9.2725 4.41406 -9.11141 cu
+4.41406 -9.11141 4.82906 -8.95016 5.03172 -8.66938 cu
+5.03172 -8.66938 5.23438 -8.38859 5.23438 -8.05656 cu
+5.23438 -8.05656 5.23438 -7.71969 5.03906 -7.46828 cu
+5.03906 -7.46828 4.84375 -7.21672 4.39453 -7.04594 cu
+4.39453 -7.04594 4.10641 -6.93359 3.12 -6.69688 cu
+3.12 -6.69688 2.13375 -6.46 1.73828 -6.25 cu
+1.73828 -6.25 1.22562 -5.98141 0.974063 -5.58344 cu
+0.974063 -5.58344 0.722656 -5.18547 0.722656 -4.69234 cu
+0.722656 -4.69234 0.722656 -4.15031 1.03031 -3.67922 cu
+1.03031 -3.67922 1.33797 -3.20797 1.92875 -2.96391 cu
+1.92875 -2.96391 2.51953 -2.71969 3.24219 -2.71969 cu
+3.24219 -2.71969 4.03812 -2.71969 4.64594 -2.97609 cu
+4.64594 -2.97609 5.25391 -3.23234 5.58109 -3.73047 cu
+5.58109 -3.73047 5.90828 -4.22844 5.93266 -4.85844 cu
+5.93266 -4.85844 5.02438 -4.92672 5.02438 -4.92672 cu
+5.02438 -4.92672 4.95125 -4.24797 4.52875 -3.90141 cu
+4.52875 -3.90141 4.10641 -3.55469 3.28125 -3.55469 cu
+3.28125 -3.55469 2.42188 -3.55469 2.02875 -3.86969 cu
+2.02875 -3.86969 1.63578 -4.18453 1.63578 -4.62891 cu
+1.63578 -4.62891 1.63578 -5.01469 1.91406 -5.26359 cu
+1.91406 -5.26359 2.1875 -5.51266 3.34219 -5.77391 cu
+3.34219 -5.77391 4.49703 -6.03516 4.92672 -6.23047 cu
+4.92672 -6.23047 5.55172 -6.51859 5.84953 -6.96047 cu
+5.84953 -6.96047 6.1475 -7.40234 6.1475 -7.97844 cu
+6.1475 -7.97844 6.1475 -8.54984 5.82031 -9.05516 cu
+5.82031 -9.05516 5.49313 -9.56047 4.88031 -9.84125 cu
+4.88031 -9.84125 4.26766 -10.122 3.50094 -10.122 cu
+3.50094 -10.122 2.52937 -10.122 1.8725 -9.83891 cu
+1.8725 -9.83891 1.21578 -9.55562 0.842187 -8.98687 cu
+0.842187 -8.98687 0.46875 -8.41797 0.449219 -7.70016 cu
+cl
+} bind def
+/G68 { newpath
+4.04297 -9.36031 m
+4.04297 -9.36031 3.55469 -9.77531 3.10297 -9.94625 cu
+3.10297 -9.94625 2.65141 -10.1172 2.13375 -10.1172 cu
+2.13375 -10.1172 1.27937 -10.1172 0.820312 -9.69969 cu
+0.820312 -9.69969 0.361406 -9.28219 0.361406 -8.63281 cu
+0.361406 -8.63281 0.361406 -8.25188 0.534687 -7.93703 cu
+0.534687 -7.93703 0.707969 -7.62203 0.98875 -7.43172 cu
+0.98875 -7.43172 1.26953 -7.24125 1.62109 -7.14359 cu
+1.62109 -7.14359 1.87984 -7.07516 2.40234 -7.01172 cu
+2.40234 -7.01172 3.46687 -6.88469 3.96969 -6.70891 cu
+3.96969 -6.70891 3.97469 -6.52828 3.97469 -6.47953 cu
+3.97469 -6.47953 3.97469 -5.94234 3.72562 -5.72266 cu
+3.72562 -5.72266 3.38875 -5.42484 2.72469 -5.42484 cu
+2.72469 -5.42484 2.10453 -5.42484 1.80906 -5.64219 cu
+1.80906 -5.64219 1.51375 -5.85938 1.37203 -6.41109 cu
+1.37203 -6.41109 0.512656 -6.29391 0.512656 -6.29391 cu
+0.512656 -6.29391 0.629844 -5.74219 0.898438 -5.40281 cu
+0.898438 -5.40281 1.16703 -5.06344 1.67484 -4.88031 cu
+1.67484 -4.88031 2.18266 -4.69719 2.85156 -4.69719 cu
+2.85156 -4.69719 3.51562 -4.69719 3.93063 -4.85344 cu
+3.93063 -4.85344 4.34578 -5.00969 4.54109 -5.24656 cu
+4.54109 -5.24656 4.73641 -5.48344 4.81453 -5.84469 cu
+4.81453 -5.84469 4.85844 -6.06937 4.85844 -6.65531 cu
+4.85844 -6.65531 4.85844 -7.82719 4.85844 -7.82719 cu
+4.85844 -7.82719 4.85844 -9.05266 4.91453 -9.3775 cu
+4.91453 -9.3775 4.97078 -9.70219 5.13672 -10 cu
+5.13672 -10 4.21875 -10 4.21875 -10 cu
+4.21875 -10 4.08203 -9.72656 4.04297 -9.36031 cu
+4.04297 -9.36031 4.04297 -9.36031 4.04297 -9.36031 cu
+cl
+3.96969 -7.3975 m
+3.96969 -7.3975 3.49125 -7.59281 2.53422 -7.72953 cu
+2.53422 -7.72953 1.99219 -7.80766 1.7675 -7.90531 cu
+1.7675 -7.90531 1.54297 -8.00297 1.42094 -8.19094 cu
+1.42094 -8.19094 1.29891 -8.37891 1.29891 -8.60844 cu
+1.29891 -8.60844 1.29891 -8.96 1.565 -9.19438 cu
+1.565 -9.19438 1.83109 -9.42875 2.34375 -9.42875 cu
+2.34375 -9.42875 2.85156 -9.42875 3.24703 -9.20656 cu
+3.24703 -9.20656 3.64266 -8.98438 3.82812 -8.59859 cu
+3.82812 -8.59859 3.96969 -8.30078 3.96969 -7.71969 cu
+3.96969 -7.71969 3.96969 -7.3975 3.96969 -7.3975 cu
+cl
+} bind def
+/G69 { newpath
+1.46969 -10 m
+1.46969 -10 0.654375 -10 0.654375 -10 cu
+0.654375 -10 0.654375 -2.84172 0.654375 -2.84172 cu
+0.654375 -2.84172 1.53328 -2.84172 1.53328 -2.84172 cu
+1.53328 -2.84172 1.53328 -5.39547 1.53328 -5.39547 cu
+1.53328 -5.39547 2.08984 -4.69719 2.95406 -4.69719 cu
+2.95406 -4.69719 3.43266 -4.69719 3.85984 -4.89016 cu
+3.85984 -4.89016 4.28719 -5.08297 4.56297 -5.43219 cu
+4.56297 -5.43219 4.83891 -5.78125 4.99516 -6.27438 cu
+4.99516 -6.27438 5.15141 -6.7675 5.15141 -7.32906 cu
+5.15141 -7.32906 5.15141 -8.66203 4.49219 -9.38969 cu
+4.49219 -9.38969 3.83297 -10.1172 2.91016 -10.1172 cu
+2.91016 -10.1172 1.99219 -10.1172 1.46969 -9.35063 cu
+1.46969 -9.35063 1.46969 -10 1.46969 -10 cu
+1.46969 -10 1.46969 -10 1.46969 -10 cu
+cl
+1.46 -7.36813 m
+1.46 -7.36813 1.46 -8.30078 1.71391 -8.71578 cu
+1.71391 -8.71578 2.12891 -9.39453 2.83688 -9.39453 cu
+2.83688 -9.39453 3.41312 -9.39453 3.83297 -8.89406 cu
+3.83297 -8.89406 4.25297 -8.39359 4.25297 -7.40234 cu
+4.25297 -7.40234 4.25297 -6.38672 3.85016 -5.90328 cu
+3.85016 -5.90328 3.44734 -5.41984 2.87594 -5.41984 cu
+2.87594 -5.41984 2.29984 -5.41984 1.87984 -5.92047 cu
+1.87984 -5.92047 1.46 -6.42094 1.46 -7.36813 cu
+cl
+} bind def
+/G70 { newpath
+4.04297 -8.10063 m
+4.04297 -8.10063 4.90719 -8.21281 4.90719 -8.21281 cu
+4.90719 -8.21281 4.76562 -9.10641 4.18203 -9.61187 cu
+4.18203 -9.61187 3.59859 -10.1172 2.74906 -10.1172 cu
+2.74906 -10.1172 1.68453 -10.1172 1.0375 -9.42141 cu
+1.0375 -9.42141 0.390625 -8.72563 0.390625 -7.42672 cu
+0.390625 -7.42672 0.390625 -6.58688 0.668906 -5.95703 cu
+0.668906 -5.95703 0.947344 -5.32719 1.51609 -5.01219 cu
+1.51609 -5.01219 2.085 -4.69719 2.75391 -4.69719 cu
+2.75391 -4.69719 3.59859 -4.69719 4.13563 -5.12453 cu
+4.13563 -5.12453 4.67281 -5.55172 4.82422 -6.33781 cu
+4.82422 -6.33781 3.96969 -6.46969 3.96969 -6.46969 cu
+3.96969 -6.46969 3.84766 -5.94719 3.5375 -5.68359 cu
+3.5375 -5.68359 3.2275 -5.41984 2.78812 -5.41984 cu
+2.78812 -5.41984 2.12406 -5.41984 1.70891 -5.89594 cu
+1.70891 -5.89594 1.29391 -6.37203 1.29391 -7.40234 cu
+1.29391 -7.40234 1.29391 -8.44719 1.69422 -8.92094 cu
+1.69422 -8.92094 2.09469 -9.39453 2.73922 -9.39453 cu
+2.73922 -9.39453 3.25687 -9.39453 3.60344 -9.07719 cu
+3.60344 -9.07719 3.95016 -8.75969 4.04297 -8.10063 cu
+cl
+} bind def
+/G71 { newpath
+4.02344 -10 m
+4.02344 -10 4.02344 -9.34563 4.02344 -9.34563 cu
+4.02344 -9.34563 3.53031 -10.1172 2.57328 -10.1172 cu
+2.57328 -10.1172 1.95312 -10.1172 1.43312 -9.77547 cu
+1.43312 -9.77547 0.913125 -9.43359 0.6275 -8.82078 cu
+0.6275 -8.82078 0.341875 -8.20797 0.341875 -7.41203 cu
+0.341875 -7.41203 0.341875 -6.63578 0.600625 -6.00344 cu
+0.600625 -6.00344 0.859375 -5.37109 1.37688 -5.03422 cu
+1.37688 -5.03422 1.89453 -4.69719 2.53422 -4.69719 cu
+2.53422 -4.69719 3.00297 -4.69719 3.36906 -4.895 cu
+3.36906 -4.895 3.73531 -5.09281 3.96484 -5.41016 cu
+3.96484 -5.41016 3.96484 -2.84172 3.96484 -2.84172 cu
+3.96484 -2.84172 4.83891 -2.84172 4.83891 -2.84172 cu
+4.83891 -2.84172 4.83891 -10 4.83891 -10 cu
+4.83891 -10 4.02344 -10 4.02344 -10 cu
+4.02344 -10 4.02344 -10 4.02344 -10 cu
+cl
+1.24516 -7.41203 m
+1.24516 -7.41203 1.24516 -8.40813 1.665 -8.90141 cu
+1.665 -8.90141 2.085 -9.39453 2.65625 -9.39453 cu
+2.65625 -9.39453 3.2325 -9.39453 3.63531 -8.92344 cu
+3.63531 -8.92344 4.03812 -8.45219 4.03812 -7.48531 cu
+4.03812 -7.48531 4.03812 -6.42094 3.62797 -5.92297 cu
+3.62797 -5.92297 3.21781 -5.42484 2.61719 -5.42484 cu
+2.61719 -5.42484 2.03125 -5.42484 1.63813 -5.90344 cu
+1.63813 -5.90344 1.24516 -6.38187 1.24516 -7.41203 cu
+cl
+} bind def
+/G72 { newpath
+4.20906 -8.33 m
+4.20906 -8.33 5.11719 -8.44234 5.11719 -8.44234 cu
+5.11719 -8.44234 4.90234 -9.23828 4.32125 -9.67781 cu
+4.32125 -9.67781 3.74031 -10.1172 2.83688 -10.1172 cu
+2.83688 -10.1172 1.69922 -10.1172 1.03266 -9.41656 cu
+1.03266 -9.41656 0.36625 -8.71578 0.36625 -7.45109 cu
+0.36625 -7.45109 0.36625 -6.1425 1.04 -5.41984 cu
+1.04 -5.41984 1.71391 -4.69719 2.78812 -4.69719 cu
+2.78812 -4.69719 3.82812 -4.69719 4.48734 -5.40531 cu
+4.48734 -5.40531 5.14656 -6.11328 5.14656 -7.3975 cu
+5.14656 -7.3975 5.14656 -7.47562 5.14156 -7.63187 cu
+5.14156 -7.63187 1.27438 -7.63187 1.27438 -7.63187 cu
+1.27438 -7.63187 1.32328 -8.48625 1.75781 -8.94047 cu
+1.75781 -8.94047 2.19234 -9.39453 2.84187 -9.39453 cu
+2.84187 -9.39453 3.32516 -9.39453 3.66688 -9.14062 cu
+3.66688 -9.14062 4.00875 -8.88672 4.20906 -8.33 cu
+4.20906 -8.33 4.20906 -8.33 4.20906 -8.33 cu
+cl
+1.32328 -6.90922 m
+1.32328 -6.90922 4.21875 -6.90922 4.21875 -6.90922 cu
+4.21875 -6.90922 4.16016 -6.25484 3.88672 -5.92766 cu
+3.88672 -5.92766 3.46687 -5.41984 2.79781 -5.41984 cu
+2.79781 -5.41984 2.19234 -5.41984 1.77969 -5.82516 cu
+1.77969 -5.82516 1.36719 -6.23047 1.32328 -6.90922 cu
+cl
+} bind def
+/G73 { newpath
+0.869219 -10 m
+0.869219 -10 0.869219 -5.49797 0.869219 -5.49797 cu
+0.869219 -5.49797 0.0928125 -5.49797 0.0928125 -5.49797 cu
+0.0928125 -5.49797 0.0928125 -4.81438 0.0928125 -4.81438 cu
+0.0928125 -4.81438 0.869219 -4.81438 0.869219 -4.81438 cu
+0.869219 -4.81438 0.869219 -4.26266 0.869219 -4.26266 cu
+0.869219 -4.26266 0.869219 -3.74016 0.961875 -3.48625 cu
+0.961875 -3.48625 1.08891 -3.14453 1.40875 -2.93219 cu
+1.40875 -2.93219 1.72859 -2.71969 2.30469 -2.71969 cu
+2.30469 -2.71969 2.67578 -2.71969 3.125 -2.80766 cu
+3.125 -2.80766 2.99313 -3.57422 2.99313 -3.57422 cu
+2.99313 -3.57422 2.71969 -3.52531 2.47562 -3.52531 cu
+2.47562 -3.52531 2.07516 -3.52531 1.90906 -3.69625 cu
+1.90906 -3.69625 1.74313 -3.86719 1.74313 -4.33594 cu
+1.74313 -4.33594 1.74313 -4.81438 1.74313 -4.81438 cu
+1.74313 -4.81438 2.75391 -4.81438 2.75391 -4.81438 cu
+2.75391 -4.81438 2.75391 -5.49797 2.75391 -5.49797 cu
+2.75391 -5.49797 1.74313 -5.49797 1.74313 -5.49797 cu
+1.74313 -5.49797 1.74313 -10 1.74313 -10 cu
+1.74313 -10 0.869219 -10 0.869219 -10 cu
+cl
+} bind def
+/G74 { newpath
+0.498125 -10.4297 m
+0.498125 -10.4297 1.3525 -10.5567 1.3525 -10.5567 cu
+1.3525 -10.5567 1.40625 -10.9522 1.65047 -11.1328 cu
+1.65047 -11.1328 1.9775 -11.377 2.54391 -11.377 cu
+2.54391 -11.377 3.15437 -11.377 3.48641 -11.1328 cu
+3.48641 -11.1328 3.81844 -10.8887 3.93562 -10.4492 cu
+3.93562 -10.4492 4.00391 -10.1806 3.99906 -9.32125 cu
+3.99906 -9.32125 3.42281 -10 2.56344 -10 cu
+2.56344 -10 1.49422 -10 0.908281 -9.22859 cu
+0.908281 -9.22859 0.322344 -8.45703 0.322344 -7.37797 cu
+0.322344 -7.37797 0.322344 -6.63578 0.590781 -6.00828 cu
+0.590781 -6.00828 0.859375 -5.38078 1.36953 -5.03906 cu
+1.36953 -5.03906 1.87984 -4.69719 2.56844 -4.69719 cu
+2.56844 -4.69719 3.48641 -4.69719 4.08203 -5.43938 cu
+4.08203 -5.43938 4.08203 -4.81438 4.08203 -4.81438 cu
+4.08203 -4.81438 4.89266 -4.81438 4.89266 -4.81438 cu
+4.89266 -4.81438 4.89266 -9.29688 4.89266 -9.29688 cu
+4.89266 -9.29688 4.89266 -10.5078 4.64594 -11.0131 cu
+4.64594 -11.0131 4.39938 -11.5186 3.86469 -11.8116 cu
+3.86469 -11.8116 3.33016 -12.1045 2.54891 -12.1045 cu
+2.54891 -12.1045 1.62109 -12.1045 1.04984 -11.687 cu
+1.04984 -11.687 0.478594 -11.2695 0.498125 -10.4297 cu
+0.498125 -10.4297 0.498125 -10.4297 0.498125 -10.4297 cu
+cl
+1.22562 -7.31438 m
+1.22562 -7.31438 1.22562 -8.335 1.63078 -8.80375 cu
+1.63078 -8.80375 2.03609 -9.2725 2.64656 -9.2725 cu
+2.64656 -9.2725 3.25203 -9.2725 3.66219 -8.80625 cu
+3.66219 -8.80625 4.07234 -8.33984 4.07234 -7.34375 cu
+4.07234 -7.34375 4.07234 -6.39156 3.64984 -5.90828 cu
+3.64984 -5.90828 3.2275 -5.42484 2.63187 -5.42484 cu
+2.63187 -5.42484 2.04594 -5.42484 1.63578 -5.90094 cu
+1.63578 -5.90094 1.22562 -6.37688 1.22562 -7.31438 cu
+cl
+} bind def
+/G75 { newpath
+0.659219 -10 m
+0.659219 -10 0.659219 -2.84172 0.659219 -2.84172 cu
+0.659219 -2.84172 1.53812 -2.84172 1.53812 -2.84172 cu
+1.53812 -2.84172 1.53812 -5.41016 1.53812 -5.41016 cu
+1.53812 -5.41016 2.15328 -4.69719 3.09078 -4.69719 cu
+3.09078 -4.69719 3.66703 -4.69719 4.09172 -4.92437 cu
+4.09172 -4.92437 4.51656 -5.15141 4.69969 -5.55187 cu
+4.69969 -5.55187 4.88281 -5.95219 4.88281 -6.71391 cu
+4.88281 -6.71391 4.88281 -10 4.88281 -10 cu
+4.88281 -10 4.00391 -10 4.00391 -10 cu
+4.00391 -10 4.00391 -6.71391 4.00391 -6.71391 cu
+4.00391 -6.71391 4.00391 -6.05469 3.71828 -5.75437 cu
+3.71828 -5.75437 3.43266 -5.45406 2.91016 -5.45406 cu
+2.91016 -5.45406 2.51953 -5.45406 2.17531 -5.65672 cu
+2.17531 -5.65672 1.83109 -5.85938 1.68453 -6.20609 cu
+1.68453 -6.20609 1.53812 -6.55266 1.53812 -7.16312 cu
+1.53812 -7.16312 1.53812 -10 1.53812 -10 cu
+1.53812 -10 0.659219 -10 0.659219 -10 cu
+cl
+} bind def
+/G76 { newpath
+0.664062 -3.8525 m
+0.664062 -3.8525 0.664062 -2.84172 0.664062 -2.84172 cu
+0.664062 -2.84172 1.54297 -2.84172 1.54297 -2.84172 cu
+1.54297 -2.84172 1.54297 -3.8525 1.54297 -3.8525 cu
+1.54297 -3.8525 0.664062 -3.8525 0.664062 -3.8525 cu
+0.664062 -3.8525 0.664062 -3.8525 0.664062 -3.8525 cu
+cl
+0.664062 -10 m
+0.664062 -10 0.664062 -4.81438 0.664062 -4.81438 cu
+0.664062 -4.81438 1.54297 -4.81438 1.54297 -4.81438 cu
+1.54297 -4.81438 1.54297 -10 1.54297 -10 cu
+1.54297 -10 0.664062 -10 0.664062 -10 cu
+cl
+} bind def
+/G78 { newpath
+0.664062 -10 m
+0.664062 -10 0.664062 -2.84172 0.664062 -2.84172 cu
+0.664062 -2.84172 1.54297 -2.84172 1.54297 -2.84172 cu
+1.54297 -2.84172 1.54297 -6.92375 1.54297 -6.92375 cu
+1.54297 -6.92375 3.62312 -4.81438 3.62312 -4.81438 cu
+3.62312 -4.81438 4.76078 -4.81438 4.76078 -4.81438 cu
+4.76078 -4.81438 2.77828 -6.73828 2.77828 -6.73828 cu
+2.77828 -6.73828 4.96094 -10 4.96094 -10 cu
+4.96094 -10 3.87703 -10 3.87703 -10 cu
+3.87703 -10 2.16312 -7.34859 2.16312 -7.34859 cu
+2.16312 -7.34859 1.54297 -7.94437 1.54297 -7.94437 cu
+1.54297 -7.94437 1.54297 -10 1.54297 -10 cu
+1.54297 -10 0.664062 -10 0.664062 -10 cu
+cl
+} bind def
+/G79 { newpath
+0.639687 -10 m
+0.639687 -10 0.639687 -2.84172 0.639687 -2.84172 cu
+0.639687 -2.84172 1.51859 -2.84172 1.51859 -2.84172 cu
+1.51859 -2.84172 1.51859 -10 1.51859 -10 cu
+1.51859 -10 0.639687 -10 0.639687 -10 cu
+cl
+} bind def
+/G80 { newpath
+0.659219 -10 m
+0.659219 -10 0.659219 -4.81438 0.659219 -4.81438 cu
+0.659219 -4.81438 1.44531 -4.81438 1.44531 -4.81438 cu
+1.44531 -4.81438 1.44531 -5.54203 1.44531 -5.54203 cu
+1.44531 -5.54203 1.68953 -5.16109 2.09469 -4.92922 cu
+2.09469 -4.92922 2.5 -4.69719 3.01766 -4.69719 cu
+3.01766 -4.69719 3.59375 -4.69719 3.96234 -4.93656 cu
+3.96234 -4.93656 4.33109 -5.17578 4.4825 -5.60547 cu
+4.4825 -5.60547 5.09766 -4.69719 6.08406 -4.69719 cu
+6.08406 -4.69719 6.85547 -4.69719 7.27047 -5.12453 cu
+7.27047 -5.12453 7.68562 -5.55172 7.68562 -6.44047 cu
+7.68562 -6.44047 7.68562 -10 7.68562 -10 cu
+7.68562 -10 6.81156 -10 6.81156 -10 cu
+6.81156 -10 6.81156 -6.73344 6.81156 -6.73344 cu
+6.81156 -6.73344 6.81156 -6.20609 6.72609 -5.97422 cu
+6.72609 -5.97422 6.64062 -5.74219 6.41594 -5.60062 cu
+6.41594 -5.60062 6.19141 -5.45891 5.88875 -5.45891 cu
+5.88875 -5.45891 5.34187 -5.45891 4.98047 -5.82281 cu
+4.98047 -5.82281 4.61922 -6.18656 4.61922 -6.98734 cu
+4.61922 -6.98734 4.61922 -10 4.61922 -10 cu
+4.61922 -10 3.74031 -10 3.74031 -10 cu
+3.74031 -10 3.74031 -6.63078 3.74031 -6.63078 cu
+3.74031 -6.63078 3.74031 -6.04484 3.52547 -5.75188 cu
+3.52547 -5.75188 3.31062 -5.45891 2.82234 -5.45891 cu
+2.82234 -5.45891 2.45125 -5.45891 2.13625 -5.65422 cu
+2.13625 -5.65422 1.82125 -5.84953 1.67969 -6.22562 cu
+1.67969 -6.22562 1.53812 -6.60156 1.53812 -7.30953 cu
+1.53812 -7.30953 1.53812 -10 1.53812 -10 cu
+1.53812 -10 0.659219 -10 0.659219 -10 cu
+cl
+} bind def
+/G81 { newpath
+0.659219 -10 m
+0.659219 -10 0.659219 -4.81438 0.659219 -4.81438 cu
+0.659219 -4.81438 1.45016 -4.81438 1.45016 -4.81438 cu
+1.45016 -4.81438 1.45016 -5.55172 1.45016 -5.55172 cu
+1.45016 -5.55172 2.02156 -4.69719 3.10062 -4.69719 cu
+3.10062 -4.69719 3.56937 -4.69719 3.96234 -4.86578 cu
+3.96234 -4.86578 4.35547 -5.03422 4.55078 -5.30766 cu
+4.55078 -5.30766 4.74609 -5.58109 4.82422 -5.95703 cu
+4.82422 -5.95703 4.87312 -6.20109 4.87312 -6.81156 cu
+4.87312 -6.81156 4.87312 -10 4.87312 -10 cu
+4.87312 -10 3.99422 -10 3.99422 -10 cu
+3.99422 -10 3.99422 -6.84563 3.99422 -6.84563 cu
+3.99422 -6.84563 3.99422 -6.30859 3.89156 -6.0425 cu
+3.89156 -6.0425 3.78906 -5.77641 3.52781 -5.61766 cu
+3.52781 -5.61766 3.26656 -5.45891 2.915 -5.45891 cu
+2.915 -5.45891 2.35359 -5.45891 1.94578 -5.81547 cu
+1.94578 -5.81547 1.53812 -6.17188 1.53812 -7.16797 cu
+1.53812 -7.16797 1.53812 -10 1.53812 -10 cu
+1.53812 -10 0.659219 -10 0.659219 -10 cu
+cl
+} bind def
+/G82 { newpath
+0.332031 -7.40719 m
+0.332031 -7.40719 0.332031 -5.96672 1.13281 -5.27344 cu
+1.13281 -5.27344 1.80172 -4.69719 2.76375 -4.69719 cu
+2.76375 -4.69719 3.83297 -4.69719 4.51172 -5.39797 cu
+4.51172 -5.39797 5.19047 -6.09859 5.19047 -7.33391 cu
+5.19047 -7.33391 5.19047 -8.335 4.89016 -8.90875 cu
+4.89016 -8.90875 4.58984 -9.48234 4.01609 -9.79984 cu
+4.01609 -9.79984 3.44234 -10.1172 2.76375 -10.1172 cu
+2.76375 -10.1172 1.67484 -10.1172 1.00344 -9.41891 cu
+1.00344 -9.41891 0.332031 -8.72063 0.332031 -7.40719 cu
+0.332031 -7.40719 0.332031 -7.40719 0.332031 -7.40719 cu
+cl
+1.23531 -7.40719 m
+1.23531 -7.40719 1.23531 -8.40328 1.66984 -8.89891 cu
+1.66984 -8.89891 2.10453 -9.39453 2.76375 -9.39453 cu
+2.76375 -9.39453 3.41797 -9.39453 3.8525 -8.89656 cu
+3.8525 -8.89656 4.28719 -8.39844 4.28719 -7.37797 cu
+4.28719 -7.37797 4.28719 -6.41594 3.85016 -5.92047 cu
+3.85016 -5.92047 3.41312 -5.42484 2.76375 -5.42484 cu
+2.76375 -5.42484 2.10453 -5.42484 1.66984 -5.91797 cu
+1.66984 -5.91797 1.23531 -6.41109 1.23531 -7.40719 cu
+cl
+} bind def
+/G83 { newpath
+0.659219 -11.9873 m
+0.659219 -11.9873 0.659219 -4.81438 0.659219 -4.81438 cu
+0.659219 -4.81438 1.46 -4.81438 1.46 -4.81438 cu
+1.46 -4.81438 1.46 -5.48828 1.46 -5.48828 cu
+1.46 -5.48828 1.74313 -5.09281 2.09953 -4.895 cu
+2.09953 -4.895 2.45609 -4.69719 2.96391 -4.69719 cu
+2.96391 -4.69719 3.62797 -4.69719 4.13578 -5.03906 cu
+4.13578 -5.03906 4.64359 -5.38078 4.90234 -6.00344 cu
+4.90234 -6.00344 5.16109 -6.62594 5.16109 -7.36813 cu
+5.16109 -7.36813 5.16109 -8.16406 4.87547 -8.80125 cu
+4.87547 -8.80125 4.58984 -9.43844 4.04531 -9.77781 cu
+4.04531 -9.77781 3.50094 -10.1172 2.90047 -10.1172 cu
+2.90047 -10.1172 2.46094 -10.1172 2.11172 -9.93172 cu
+2.11172 -9.93172 1.76266 -9.74609 1.53812 -9.46281 cu
+1.53812 -9.46281 1.53812 -11.9873 1.53812 -11.9873 cu
+1.53812 -11.9873 0.659219 -11.9873 0.659219 -11.9873 cu
+0.659219 -11.9873 0.659219 -11.9873 0.659219 -11.9873 cu
+cl
+1.45516 -7.43656 m
+1.45516 -7.43656 1.45516 -8.4375 1.86031 -8.91609 cu
+1.86031 -8.91609 2.26562 -9.39453 2.84187 -9.39453 cu
+2.84187 -9.39453 3.42781 -9.39453 3.84516 -8.89891 cu
+3.84516 -8.89891 4.26266 -8.40328 4.26266 -7.36328 cu
+4.26266 -7.36328 4.26266 -6.37203 3.855 -5.87891 cu
+3.855 -5.87891 3.44734 -5.38578 2.88094 -5.38578 cu
+2.88094 -5.38578 2.31937 -5.38578 1.88719 -5.91062 cu
+1.88719 -5.91062 1.45516 -6.43547 1.45516 -7.43656 cu
+cl
+} bind def
+/G85 { newpath
+0.649375 -10 m
+0.649375 -10 0.649375 -4.81438 0.649375 -4.81438 cu
+0.649375 -4.81438 1.44047 -4.81438 1.44047 -4.81438 cu
+1.44047 -4.81438 1.44047 -5.60062 1.44047 -5.60062 cu
+1.44047 -5.60062 1.74313 -5.04875 1.99953 -4.87297 cu
+1.99953 -4.87297 2.25594 -4.69719 2.56344 -4.69719 cu
+2.56344 -4.69719 3.00781 -4.69719 3.46687 -4.98047 cu
+3.46687 -4.98047 3.16406 -5.79594 3.16406 -5.79594 cu
+3.16406 -5.79594 2.84187 -5.60547 2.51953 -5.60547 cu
+2.51953 -5.60547 2.23141 -5.60547 2.00188 -5.77891 cu
+2.00188 -5.77891 1.7725 -5.95219 1.67484 -6.25969 cu
+1.67484 -6.25969 1.52828 -6.72844 1.52828 -7.28516 cu
+1.52828 -7.28516 1.52828 -10 1.52828 -10 cu
+1.52828 -10 0.649375 -10 0.649375 -10 cu
+cl
+} bind def
+/G86 { newpath
+0.307656 -8.45219 m
+0.307656 -8.45219 1.17672 -8.31547 1.17672 -8.31547 cu
+1.17672 -8.31547 1.25 -8.83781 1.58438 -9.11625 cu
+1.58438 -9.11625 1.91891 -9.39453 2.51953 -9.39453 cu
+2.51953 -9.39453 3.125 -9.39453 3.41797 -9.14797 cu
+3.41797 -9.14797 3.71094 -8.90141 3.71094 -8.56938 cu
+3.71094 -8.56938 3.71094 -8.27141 3.45219 -8.10063 cu
+3.45219 -8.10063 3.27156 -7.98344 2.55375 -7.80266 cu
+2.55375 -7.80266 1.58688 -7.55859 1.21328 -7.38047 cu
+1.21328 -7.38047 0.839844 -7.20219 0.646875 -6.88719 cu
+0.646875 -6.88719 0.454063 -6.57219 0.454063 -6.19141 cu
+0.454063 -6.19141 0.454063 -5.84469 0.612812 -5.54937 cu
+0.612812 -5.54937 0.771563 -5.25391 1.045 -5.05859 cu
+1.045 -5.05859 1.25 -4.90719 1.60391 -4.80219 cu
+1.60391 -4.80219 1.95797 -4.69719 2.36328 -4.69719 cu
+2.36328 -4.69719 2.97359 -4.69719 3.435 -4.87297 cu
+3.435 -4.87297 3.89656 -5.04875 4.11625 -5.34906 cu
+4.11625 -5.34906 4.33594 -5.64938 4.41891 -6.15234 cu
+4.41891 -6.15234 3.55953 -6.26953 3.55953 -6.26953 cu
+3.55953 -6.26953 3.50094 -5.86906 3.22016 -5.64453 cu
+3.22016 -5.64453 2.93953 -5.41984 2.42672 -5.41984 cu
+2.42672 -5.41984 1.82125 -5.41984 1.5625 -5.62016 cu
+1.5625 -5.62016 1.30375 -5.82031 1.30375 -6.08891 cu
+1.30375 -6.08891 1.30375 -6.25969 1.41109 -6.39641 cu
+1.41109 -6.39641 1.51859 -6.53812 1.74812 -6.63078 cu
+1.74812 -6.63078 1.87984 -6.67969 2.52438 -6.85547 cu
+2.52438 -6.85547 3.45703 -7.10453 3.82563 -7.26328 cu
+3.82563 -7.26328 4.19437 -7.42188 4.40422 -7.72469 cu
+4.40422 -7.72469 4.61422 -8.02734 4.61422 -8.47656 cu
+4.61422 -8.47656 4.61422 -8.91594 4.35781 -9.30422 cu
+4.35781 -9.30422 4.10156 -9.69234 3.61813 -9.90484 cu
+3.61813 -9.90484 3.13484 -10.1172 2.52438 -10.1172 cu
+2.52438 -10.1172 1.51375 -10.1172 0.983906 -9.69734 cu
+0.983906 -9.69734 0.454063 -9.27734 0.307656 -8.45219 cu
+cl
+} bind def
+/G87 { newpath
+2.57812 -9.21391 m
+2.57812 -9.21391 2.70516 -9.99016 2.70516 -9.99016 cu
+2.70516 -9.99016 2.33406 -10.0684 2.04109 -10.0684 cu
+2.04109 -10.0684 1.5625 -10.0684 1.29875 -9.91703 cu
+1.29875 -9.91703 1.03516 -9.76562 0.927656 -9.51906 cu
+0.927656 -9.51906 0.820312 -9.2725 0.820312 -8.48141 cu
+0.820312 -8.48141 0.820312 -5.49797 0.820312 -5.49797 cu
+0.820312 -5.49797 0.175781 -5.49797 0.175781 -5.49797 cu
+0.175781 -5.49797 0.175781 -4.81438 0.175781 -4.81438 cu
+0.175781 -4.81438 0.820312 -4.81438 0.820312 -4.81438 cu
+0.820312 -4.81438 0.820312 -3.53031 0.820312 -3.53031 cu
+0.820312 -3.53031 1.69437 -3.00297 1.69437 -3.00297 cu
+1.69437 -3.00297 1.69437 -4.81438 1.69437 -4.81438 cu
+1.69437 -4.81438 2.57812 -4.81438 2.57812 -4.81438 cu
+2.57812 -4.81438 2.57812 -5.49797 2.57812 -5.49797 cu
+2.57812 -5.49797 1.69437 -5.49797 1.69437 -5.49797 cu
+1.69437 -5.49797 1.69437 -8.53031 1.69437 -8.53031 cu
+1.69437 -8.53031 1.69437 -8.90625 1.74078 -9.01375 cu
+1.74078 -9.01375 1.78719 -9.12109 1.89203 -9.18453 cu
+1.89203 -9.18453 1.99703 -9.24797 2.19234 -9.24797 cu
+2.19234 -9.24797 2.33891 -9.24797 2.57812 -9.21391 cu
+cl
+} bind def
+/G88 { newpath
+4.05766 -10 m
+4.05766 -10 4.05766 -9.23828 4.05766 -9.23828 cu
+4.05766 -9.23828 3.45219 -10.1172 2.41219 -10.1172 cu
+2.41219 -10.1172 1.95312 -10.1172 1.55516 -9.94141 cu
+1.55516 -9.94141 1.15719 -9.76562 0.964375 -9.49953 cu
+0.964375 -9.49953 0.771563 -9.23344 0.693438 -8.84766 cu
+0.693438 -8.84766 0.639687 -8.58891 0.639687 -8.02734 cu
+0.639687 -8.02734 0.639687 -4.81438 0.639687 -4.81438 cu
+0.639687 -4.81438 1.51859 -4.81438 1.51859 -4.81438 cu
+1.51859 -4.81438 1.51859 -7.69047 1.51859 -7.69047 cu
+1.51859 -7.69047 1.51859 -8.37891 1.57234 -8.61812 cu
+1.57234 -8.61812 1.65531 -8.96484 1.92375 -9.16266 cu
+1.92375 -9.16266 2.19234 -9.36031 2.58797 -9.36031 cu
+2.58797 -9.36031 2.98344 -9.36031 3.33 -9.15766 cu
+3.33 -9.15766 3.67672 -8.955 3.82078 -8.60594 cu
+3.82078 -8.60594 3.96484 -8.25688 3.96484 -7.59281 cu
+3.96484 -7.59281 3.96484 -4.81438 3.96484 -4.81438 cu
+3.96484 -4.81438 4.84375 -4.81438 4.84375 -4.81438 cu
+4.84375 -4.81438 4.84375 -10 4.84375 -10 cu
+4.84375 -10 4.05766 -10 4.05766 -10 cu
+cl
+} bind def
+/G89 { newpath
+2.09969 -10 m
+2.09969 -10 0.127031 -4.81438 0.127031 -4.81438 cu
+0.127031 -4.81438 1.05469 -4.81438 1.05469 -4.81438 cu
+1.05469 -4.81438 2.16797 -7.91984 2.16797 -7.91984 cu
+2.16797 -7.91984 2.34859 -8.42281 2.5 -8.96484 cu
+2.5 -8.96484 2.61719 -8.55469 2.82719 -7.97844 cu
+2.82719 -7.97844 3.97953 -4.81438 3.97953 -4.81438 cu
+3.97953 -4.81438 4.88281 -4.81438 4.88281 -4.81438 cu
+4.88281 -4.81438 2.92 -10 2.92 -10 cu
+2.92 -10 2.09969 -10 2.09969 -10 cu
+cl
+} bind def
+end
+Scribusdict begin
+%%EndSetup
+%%Page: 1 1
+%%PageOrientation: Landscape
+%%PageBoundingBox: 0 0 595 420
+%%PageCropBox: 0 0 595.28 419.53
+save
+/DeviceCMYK setcolorspace
+0 0 tr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+383 396.53 tr
+0 0 m
+0 0 196 0 196 0 curveto
+196 0 196 -372 196 -372 curveto
+196 -372 0 -372 0 -372 curveto
+0 -372 0 0 0 0 curveto
+cl
+0 0 0.0980392 0.0392157 cmyk eofill
+0 0 m
+0 0 196 0 196 0 curveto
+196 0 196 -372 196 -372 curveto
+196 -372 0 -372 0 -372 curveto
+0 -372 0 0 0 0 curveto
+cl
+0 0 0 1 cmyk st
+gr
+gs
+1.9963 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+419 211.53 tr
+0 0 m
+0 0 116 0 116 0 curveto
+116 0 116 -122 116 -122 curveto
+116 -122 0 -122 0 -122 curveto
+0 -122 0 0 0 0 curveto
+cl
+0 0.0862745 0.184314 0.196078 cmyk eofill
+0 0 m
+0 0 116 0 116 0 curveto
+116 0 116 -122 116 -122 curveto
+116 -122 0 -122 0 -122 curveto
+0 -122 0 0 0 0 curveto
+cl
+0 0 0 1 cmyk st
+gr
+gs
+1.9963 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+438.65 204.604 tr
+gs
+17.4229 1.26367 tr
+0 0 0 1 cmyk (G54) ArialMT 1.2 shgf
+gr
+gs
+25.4268 1.26367 tr
+0 0 0 1 cmyk (G87) ArialMT 1.2 shgf
+gr
+gs
+28.7607 1.26367 tr
+0 0 0 1 cmyk (G82) ArialMT 1.2 shgf
+gr
+gs
+35.4346 1.26367 tr
+0 0 0 1 cmyk (G85) ArialMT 1.2 shgf
+gr
+gs
+39.4307 1.26367 tr
+0 0 0 1 cmyk (G68) ArialMT 1.2 shgf
+gr
+gs
+46.1045 1.26367 tr
+0 0 0 1 cmyk (G74) ArialMT 1.2 shgf
+gr
+gs
+52.7783 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+36 186.53 tr
+144 -21 m
+144 -32.5984 111.766 -42 72 -42 curveto
+32.2355 -42 0 -32.5984 0 -21 curveto
+0 -9.40202 32.2355 0 72 0 curveto
+111.766 0 144 -9.40202 144 -21 curveto
+cl
+0.403922 0 0.137255 0.196078 cmyk eofill
+144 -21 m
+144 -32.5984 111.766 -42 72 -42 curveto
+32.2355 -42 0 -32.5984 0 -21 curveto
+0 -9.40202 32.2355 0 72 0 curveto
+111.766 0 144 -9.40202 144 -21 curveto
+cl
+0 0 0 1 cmyk st
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+58 173.53 tr
+gs
+1 1.26367 tr
+0 0 0 1 cmyk (G36) ArialMT 1.2 shgf
+gr
+gs
+9.00391 1.26367 tr
+0 0 0 1 cmyk (G74) ArialMT 1.2 shgf
+gr
+gs
+15.6777 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gs
+22.3516 1.26367 tr
+0 0 0 1 cmyk (G81) ArialMT 1.2 shgf
+gr
+gs
+29.0254 1.26367 tr
+0 0 0 1 cmyk (G87) ArialMT 1.2 shgf
+gr
+gs
+35.6934 1.26367 tr
+0 0 0 1 cmyk (G11) ArialMT 1.2 shgf
+gr
+gs
+39.6895 1.26367 tr
+0 0 0 1 cmyk (G53) ArialMT 1.2 shgf
+gr
+gs
+48.3555 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gs
+55.0293 1.26367 tr
+0 0 0 1 cmyk (G86) ArialMT 1.2 shgf
+gr
+gs
+61.0293 1.26367 tr
+0 0 0 1 cmyk (G82) ArialMT 1.2 shgf
+gr
+gs
+67.7031 1.26367 tr
+0 0 0 1 cmyk (G88) ArialMT 1.2 shgf
+gr
+gs
+74.377 1.26367 tr
+0 0 0 1 cmyk (G85) ArialMT 1.2 shgf
+gr
+gs
+78.373 1.26367 tr
+0 0 0 1 cmyk (G70) ArialMT 1.2 shgf
+gr
+gs
+84.373 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gs
+91.0469 1.26367 tr
+0 0 0 1 cmyk (G12) ArialMT 1.2 shgf
+gr
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+424 392.53 tr
+0 0 m
+0 0 107 0 107 0 curveto
+107 0 107 -47 107 -47 curveto
+107 -47 0 -47 0 -47 curveto
+0 -47 0 0 0 0 curveto
+cl
+0.32549 0.0313725 0 0.196078 cmyk eofill
+0 0 m
+0 0 107 0 107 0 curveto
+107 0 107 -47 107 -47 curveto
+107 -47 0 -47 0 -47 curveto
+0 -47 0 0 0 0 curveto
+cl
+0 0 0 1 cmyk st
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+444 378.53 tr
+gs
+14.6582 1.26367 tr
+0 0 0 1 cmyk (G38) ArialMT 1.2 shgf
+gr
+gs
+23.3242 1.26367 tr
+0 0 0 1 cmyk (G82) ArialMT 1.2 shgf
+gr
+gs
+29.998 1.26367 tr
+0 0 0 1 cmyk (G81) ArialMT 1.2 shgf
+gr
+gs
+36.6719 1.26367 tr
+0 0 0 1 cmyk (G87) ArialMT 1.2 shgf
+gr
+gs
+40.0059 1.26367 tr
+0 0 0 1 cmyk (G85) ArialMT 1.2 shgf
+gr
+gs
+44.002 1.26367 tr
+0 0 0 1 cmyk (G82) ArialMT 1.2 shgf
+gr
+gs
+50.6758 1.26367 tr
+0 0 0 1 cmyk (G79) ArialMT 1.2 shgf
+gr
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+258 239.53 tr
+-145.751 ro
+0 0 m
+113.719 0 li
+0 0 0 1 cmyk st
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+262 251.53 tr
+-170.059 ro
+0 0 m
+98.4784 0 li
+0 0 0 1 cmyk st
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+22 254.53 tr
+144 -21 m
+144 -32.5984 111.766 -42 72 -42 curveto
+32.2355 -42 0 -32.5984 0 -21 curveto
+0 -9.40202 32.2355 0 72 0 curveto
+111.766 0 144 -9.40202 144 -21 curveto
+cl
+0.403922 0 0.137255 0.196078 cmyk eofill
+144 -21 m
+144 -32.5984 111.766 -42 72 -42 curveto
+32.2355 -42 0 -32.5984 0 -21 curveto
+0 -9.40202 32.2355 0 72 0 curveto
+111.766 0 144 -9.40202 144 -21 curveto
+cl
+0 0 0 1 cmyk st
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+44 241.53 tr
+gs
+1 1.26367 tr
+0 0 0 1 cmyk (G36) ArialMT 1.2 shgf
+gr
+gs
+9.00391 1.26367 tr
+0 0 0 1 cmyk (G74) ArialMT 1.2 shgf
+gr
+gs
+15.6777 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gs
+22.3516 1.26367 tr
+0 0 0 1 cmyk (G81) ArialMT 1.2 shgf
+gr
+gs
+29.0254 1.26367 tr
+0 0 0 1 cmyk (G87) ArialMT 1.2 shgf
+gr
+gs
+35.6934 1.26367 tr
+0 0 0 1 cmyk (G11) ArialMT 1.2 shgf
+gr
+gs
+39.6895 1.26367 tr
+0 0 0 1 cmyk (G53) ArialMT 1.2 shgf
+gr
+gs
+48.3555 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gs
+55.0293 1.26367 tr
+0 0 0 1 cmyk (G86) ArialMT 1.2 shgf
+gr
+gs
+61.0293 1.26367 tr
+0 0 0 1 cmyk (G82) ArialMT 1.2 shgf
+gr
+gs
+67.7031 1.26367 tr
+0 0 0 1 cmyk (G88) ArialMT 1.2 shgf
+gr
+gs
+74.377 1.26367 tr
+0 0 0 1 cmyk (G85) ArialMT 1.2 shgf
+gr
+gs
+78.373 1.26367 tr
+0 0 0 1 cmyk (G70) ArialMT 1.2 shgf
+gr
+gs
+84.373 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gs
+91.0469 1.26367 tr
+0 0 0 1 cmyk (G12) ArialMT 1.2 shgf
+gr
+gr
+gs
+430.5 346.03 m
+430.5 346.03 524.5 346.03 524.5 346.03 curveto
+524.5 346.03 524.5 319.03 524.5 319.03 curveto
+524.5 319.03 430.5 319.03 430.5 319.03 curveto
+430.5 319.03 430.5 346.03 430.5 346.03 curveto
+cl
+eoclip newpath
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+431 345.53 tr
+0 0 m
+0 0 93 0 93 0 curveto
+93 0 93 -26 93 -26 curveto
+93 -26 0 -26 0 -26 curveto
+0 -26 0 0 0 0 curveto
+cl
+0.376471 0.0352941 0 0.0666667 cmyk eofill
+0 0 m
+0 0 93 0 93 0 curveto
+93 0 93 -26 93 -26 curveto
+93 -26 0 -26 0 -26 curveto
+0 -26 0 0 0 0 curveto
+cl
+0 0 0 1 cmyk st
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+436 341.53 tr
+gs
+1.65039 1.26367 tr
+0 0 0 1 cmyk (G39) ArialMT 1.2 shgf
+gr
+gs
+10.3164 1.26367 tr
+0 0 0 1 cmyk (G16) ArialMT 1.2 shgf
+gr
+gs
+14.3125 1.26367 tr
+0 0 0 1 cmyk (G37) ArialMT 1.2 shgf
+gr
+gs
+22.3164 1.26367 tr
+0 0 0 1 cmyk (G88) ArialMT 1.2 shgf
+gr
+gs
+28.9902 1.26367 tr
+0 0 0 1 cmyk (G86) ArialMT 1.2 shgf
+gr
+gs
+38.3242 1.26367 tr
+0 0 0 1 cmyk (G76) ArialMT 1.2 shgf
+gr
+gs
+40.9902 1.26367 tr
+0 0 0 1 cmyk (G81) ArialMT 1.2 shgf
+gr
+gs
+47.6641 1.26367 tr
+0 0 0 1 cmyk (G87) ArialMT 1.2 shgf
+gr
+gs
+50.998 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gs
+57.6719 1.26367 tr
+0 0 0 1 cmyk (G85) ArialMT 1.2 shgf
+gr
+gs
+61.668 1.26367 tr
+0 0 0 1 cmyk (G73) ArialMT 1.2 shgf
+gr
+gs
+65.002 1.26367 tr
+0 0 0 1 cmyk (G68) ArialMT 1.2 shgf
+gr
+gs
+71.6758 1.26367 tr
+0 0 0 1 cmyk (G70) ArialMT 1.2 shgf
+gr
+gs
+77.6758 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gr
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+411 46.53 tr
+gs
+25.9785 1.26367 tr
+0 0 0 1 cmyk (G36) ArialMT 1.2 shgf
+gr
+gs
+33.9824 1.26367 tr
+0 0 0 1 cmyk (G78) ArialMT 1.2 shgf
+gr
+gs
+39.9824 1.26367 tr
+0 0 0 1 cmyk (G82) ArialMT 1.2 shgf
+gr
+gs
+46.6562 1.26367 tr
+0 0 0 1 cmyk (G81) ArialMT 1.2 shgf
+gr
+gs
+53.3301 1.26367 tr
+0 0 0 1 cmyk (G68) ArialMT 1.2 shgf
+gr
+gs
+60.0039 1.26367 tr
+0 0 0 1 cmyk (G71) ArialMT 1.2 shgf
+gr
+gs
+66.6777 1.26367 tr
+0 0 0 1 cmyk (G76) ArialMT 1.2 shgf
+gr
+gs
+72.6777 1.26367 tr
+0 0 0 1 cmyk (G54) ArialMT 1.2 shgf
+gr
+gs
+80.6816 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gs
+87.3555 1.26367 tr
+0 0 0 1 cmyk (G85) ArialMT 1.2 shgf
+gr
+gs
+91.3516 1.26367 tr
+0 0 0 1 cmyk (G89) ArialMT 1.2 shgf
+gr
+gs
+97.3516 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gs
+104.025 1.26367 tr
+0 0 0 1 cmyk (G85) ArialMT 1.2 shgf
+gr
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+179 305.53 tr
+0.516164 ro
+0 0 m
+111.005 0 li
+0 0 0 1 cmyk st
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+154 364.53 tr
+-18.9465 ro
+0 0 m
+141.676 0 li
+0 0 0 1 cmyk st
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[6 2] 0 setdash
+333 294.53 tr
+11.8887 ro
+0 0 m
+116.499 0 li
+0 0 0 1 cmyk st
+gr
+gs
+1.25362 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+288 388.53 tr
+0 0 m
+0 0 46 0 46 0 curveto
+46 0 46 -279 46 -279 curveto
+46 -279 0 -279 0 -279 curveto
+0 -279 0 0 0 0 curveto
+cl
+0.196078 0 0.196078 0.196078 cmyk eofill
+0 0 m
+0 0 46 0 46 0 curveto
+46 0 46 -279 46 -279 curveto
+46 -279 0 -279 0 -279 curveto
+0 -279 0 0 0 0 curveto
+cl
+0 0 0 1 cmyk st
+gr
+gs
+1.25362 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+301 198.171 tr
+90 ro
+gs
+1 1.41016 tr
+0 0 0 1 cmyk (G79) ArialMT 1.2 shgf
+gr
+gs
+3.66602 1.41016 tr
+0 0 0 1 cmyk (G76) ArialMT 1.2 shgf
+gr
+gs
+6.33203 1.41016 tr
+0 0 0 1 cmyk (G69) ArialMT 1.2 shgf
+gr
+gs
+13.0059 1.41016 tr
+0 0 0 1 cmyk (G68) ArialMT 1.2 shgf
+gr
+gs
+19.6797 1.41016 tr
+0 0 0 1 cmyk (G78) ArialMT 1.2 shgf
+gr
+gs
+25.6797 1.41016 tr
+0 0 0 1 cmyk (G82) ArialMT 1.2 shgf
+gr
+gs
+32.3535 1.41016 tr
+0 0 0 1 cmyk (G81) ArialMT 1.2 shgf
+gr
+gs
+39.0273 1.41016 tr
+0 0 0 1 cmyk (G68) ArialMT 1.2 shgf
+gr
+gs
+45.7012 1.41016 tr
+0 0 0 1 cmyk (G71) ArialMT 1.2 shgf
+gr
+gs
+52.375 1.41016 tr
+0 0 0 1 cmyk (G76) ArialMT 1.2 shgf
+gr
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+333 256.53 tr
+-19.5367 ro
+0 0 m
+98.6813 0 li
+0 0 0 1 cmyk st
+gr
+gs
+420.45 241.08 m
+420.45 241.08 533 241.08 533 241.08 curveto
+533 241.08 533 210.981 533 210.981 curveto
+533 210.981 420.45 210.981 420.45 210.981 curveto
+420.45 210.981 420.45 241.08 420.45 241.08 curveto
+cl
+eoclip newpath
+gs
+1.11137 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+421.012 240.53 tr
+0 0 m
+0 0 111.426 0 111.426 0 curveto
+111.426 0 111.426 -29 111.426 -29 curveto
+111.426 -29 0 -29 0 -29 curveto
+0 -29 0 0 0 0 curveto
+cl
+0 0.0980392 0.215686 0.0666667 cmyk eofill
+0 0 m
+0 0 111.426 0 111.426 0 curveto
+111.426 0 111.426 -29 111.426 -29 curveto
+111.426 -29 0 -29 0 -29 curveto
+0 -29 0 0 0 0 curveto
+cl
+0 0 0 1 cmyk st
+gr
+gs
+1.11137 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+426.528 234.085 tr
+gs
+12.0723 1.26367 tr
+0 0 0 1 cmyk (G44) ArialMT 1.2 shgf
+gr
+gs
+15.4062 1.26367 tr
+0 0 0 1 cmyk (G48) ArialMT 1.2 shgf
+gr
+gs
+25.4023 1.26367 tr
+0 0 0 1 cmyk (G36) ArialMT 1.2 shgf
+gr
+gs
+33.4062 1.26367 tr
+0 0 0 1 cmyk (G51) ArialMT 1.2 shgf
+gr
+gs
+44.5273 1.26367 tr
+0 0 0 1 cmyk (G76) ArialMT 1.2 shgf
+gr
+gs
+47.1934 1.26367 tr
+0 0 0 1 cmyk (G81) ArialMT 1.2 shgf
+gr
+gs
+53.8672 1.26367 tr
+0 0 0 1 cmyk (G87) ArialMT 1.2 shgf
+gr
+gs
+57.2012 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gs
+63.875 1.26367 tr
+0 0 0 1 cmyk (G85) ArialMT 1.2 shgf
+gr
+gs
+67.8711 1.26367 tr
+0 0 0 1 cmyk (G73) ArialMT 1.2 shgf
+gr
+gs
+71.2051 1.26367 tr
+0 0 0 1 cmyk (G68) ArialMT 1.2 shgf
+gr
+gs
+77.8789 1.26367 tr
+0 0 0 1 cmyk (G70) ArialMT 1.2 shgf
+gr
+gs
+83.8789 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gr
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+256 195.53 tr
+0 0 m
+0 0 31 0 31 0 curveto
+31 0 31 -72.7448 31 -72.7448 curveto
+31 -72.7448 0 -72.7448 0 -72.7448 curveto
+0 -72.7448 0 0 0 0 curveto
+cl
+0.27451 0 0.0901961 0.454902 cmyk eofill
+0 0 m
+0 0 31 0 31 0 curveto
+31 0 31 -72.7448 31 -72.7448 curveto
+31 -72.7448 0 -72.7448 0 -72.7448 curveto
+0 -72.7448 0 0 0 0 curveto
+cl
+0 0 0 1 cmyk st
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+262 136.53 tr
+90 ro
+gs
+5.32324 1.41016 tr
+0 0 0 1 cmyk (G79) ArialMT 1.2 shgf
+gr
+gs
+7.98926 1.41016 tr
+0 0 0 1 cmyk (G76) ArialMT 1.2 shgf
+gr
+gs
+10.6553 1.41016 tr
+0 0 0 1 cmyk (G69) ArialMT 1.2 shgf
+gr
+gs
+17.3291 1.41016 tr
+0 0 0 1 cmyk (G78) ArialMT 1.2 shgf
+gr
+gs
+23.3291 1.41016 tr
+0 0 0 1 cmyk (G68) ArialMT 1.2 shgf
+gr
+gs
+30.0029 1.41016 tr
+0 0 0 1 cmyk (G69) ArialMT 1.2 shgf
+gr
+gs
+36.6768 1.41016 tr
+0 0 0 1 cmyk (G70) ArialMT 1.2 shgf
+gr
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+124 120.53 tr
+0 -13.75 m
+0 -13.75 23 -13.75 23 -13.75 curveto
+23 -13.75 23 0 23 0 curveto
+23 0 69 0 69 0 curveto
+69 0 69 -13.75 69 -13.75 curveto
+69 -13.75 92 -13.75 92 -13.75 curveto
+92 -13.75 92 -41.25 92 -41.25 curveto
+92 -41.25 69 -41.25 69 -41.25 curveto
+69 -41.25 69 -55 69 -55 curveto
+69 -55 23 -55 23 -55 curveto
+23 -55 23 -41.25 23 -41.25 curveto
+23 -41.25 0 -41.25 0 -41.25 curveto
+0 -41.25 0 -13.75 0 -13.75 curveto
+cl
+0.0588235 0 0 0 cmyk eofill
+0 -13.75 m
+0 -13.75 23 -13.75 23 -13.75 curveto
+23 -13.75 23 0 23 0 curveto
+23 0 69 0 69 0 curveto
+69 0 69 -13.75 69 -13.75 curveto
+69 -13.75 92 -13.75 92 -13.75 curveto
+92 -13.75 92 -41.25 92 -41.25 curveto
+92 -41.25 69 -41.25 69 -41.25 curveto
+69 -41.25 69 -55 69 -55 curveto
+69 -55 23 -55 23 -55 curveto
+23 -55 23 -41.25 23 -41.25 curveto
+23 -41.25 0 -41.25 0 -41.25 curveto
+0 -41.25 0 -13.75 0 -13.75 curveto
+cl
+0 0 0 1 cmyk st
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+132 100.53 tr
+gs
+6.64746 1.41016 tr
+0 0 0 1 cmyk (G36) ArialMT 1.2 shgf
+gr
+gs
+14.6514 1.41016 tr
+0 0 0 1 cmyk (G83) ArialMT 1.2 shgf
+gr
+gs
+21.3252 1.41016 tr
+0 0 0 1 cmyk (G83) ArialMT 1.2 shgf
+gr
+gs
+27.999 1.41016 tr
+0 0 0 1 cmyk (G79) ArialMT 1.2 shgf
+gr
+gs
+30.665 1.41016 tr
+0 0 0 1 cmyk (G76) ArialMT 1.2 shgf
+gr
+gs
+33.3311 1.41016 tr
+0 0 0 1 cmyk (G70) ArialMT 1.2 shgf
+gr
+gs
+39.3311 1.41016 tr
+0 0 0 1 cmyk (G68) ArialMT 1.2 shgf
+gr
+gs
+46.0049 1.41016 tr
+0 0 0 1 cmyk (G87) ArialMT 1.2 shgf
+gr
+gs
+49.3389 1.41016 tr
+0 0 0 1 cmyk (G76) ArialMT 1.2 shgf
+gr
+gs
+52.0049 1.41016 tr
+0 0 0 1 cmyk (G82) ArialMT 1.2 shgf
+gr
+gs
+58.6787 1.41016 tr
+0 0 0 1 cmyk (G81) ArialMT 1.2 shgf
+gr
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+201 69.53 tr
+0 -13.75 m
+0 -13.75 23 -13.75 23 -13.75 curveto
+23 -13.75 23 0 23 0 curveto
+23 0 69 0 69 0 curveto
+69 0 69 -13.75 69 -13.75 curveto
+69 -13.75 92 -13.75 92 -13.75 curveto
+92 -13.75 92 -41.25 92 -41.25 curveto
+92 -41.25 69 -41.25 69 -41.25 curveto
+69 -41.25 69 -55 69 -55 curveto
+69 -55 23 -55 23 -55 curveto
+23 -55 23 -41.25 23 -41.25 curveto
+23 -41.25 0 -41.25 0 -41.25 curveto
+0 -41.25 0 -13.75 0 -13.75 curveto
+cl
+0.0313725 0 0 0.454902 cmyk eofill
+0 -13.75 m
+0 -13.75 23 -13.75 23 -13.75 curveto
+23 -13.75 23 0 23 0 curveto
+23 0 69 0 69 0 curveto
+69 0 69 -13.75 69 -13.75 curveto
+69 -13.75 92 -13.75 92 -13.75 curveto
+92 -13.75 92 -41.25 92 -41.25 curveto
+92 -41.25 69 -41.25 69 -41.25 curveto
+69 -41.25 69 -55 69 -55 curveto
+69 -55 23 -55 23 -55 curveto
+23 -55 23 -41.25 23 -41.25 curveto
+23 -41.25 0 -41.25 0 -41.25 curveto
+0 -41.25 0 -13.75 0 -13.75 curveto
+cl
+0 0 0 1 cmyk st
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+209 49.53 tr
+gs
+4.98047 1.26367 tr
+0 0 0 1 cmyk (G38) ArialMT 1.2 shgf
+gr
+gs
+13.6465 1.26367 tr
+0 0 0 1 cmyk (G82) ArialMT 1.2 shgf
+gr
+gs
+20.3203 1.26367 tr
+0 0 0 1 cmyk (G80) ArialMT 1.2 shgf
+gr
+gs
+30.3164 1.26367 tr
+0 0 0 1 cmyk (G83) ArialMT 1.2 shgf
+gr
+gs
+36.9902 1.26367 tr
+0 0 0 1 cmyk (G82) ArialMT 1.2 shgf
+gr
+gs
+43.6641 1.26367 tr
+0 0 0 1 cmyk (G81) ArialMT 1.2 shgf
+gr
+gs
+50.3379 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gs
+57.0117 1.26367 tr
+0 0 0 1 cmyk (G81) ArialMT 1.2 shgf
+gr
+gs
+63.6855 1.26367 tr
+0 0 0 1 cmyk (G87) ArialMT 1.2 shgf
+gr
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+170 120.53 tr
+48.2961 ro
+0 0 m
+135.281 0 li
+0 0 0 1 cmyk st
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+256 268.53 tr
+0 0 m
+0 0 31 0 31 0 curveto
+31 0 31 -72.7448 31 -72.7448 curveto
+31 -72.7448 0 -72.7448 0 -72.7448 curveto
+0 -72.7448 0 0 0 0 curveto
+cl
+0.27451 0 0.0901961 0.454902 cmyk eofill
+0 0 m
+0 0 31 0 31 0 curveto
+31 0 31 -72.7448 31 -72.7448 curveto
+31 -72.7448 0 -72.7448 0 -72.7448 curveto
+0 -72.7448 0 0 0 0 curveto
+cl
+0 0 0 1 cmyk st
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+262 209.53 tr
+90 ro
+gs
+7.32715 1.41016 tr
+0 0 0 1 cmyk (G79) ArialMT 1.2 shgf
+gr
+gs
+9.99316 1.41016 tr
+0 0 0 1 cmyk (G76) ArialMT 1.2 shgf
+gr
+gs
+12.6592 1.41016 tr
+0 0 0 1 cmyk (G69) ArialMT 1.2 shgf
+gr
+gs
+19.333 1.41016 tr
+0 0 0 1 cmyk (G78) ArialMT 1.2 shgf
+gr
+gs
+25.333 1.41016 tr
+0 0 0 1 cmyk (G70) ArialMT 1.2 shgf
+gr
+gs
+31.333 1.41016 tr
+0 0 0 1 cmyk (G68) ArialMT 1.2 shgf
+gr
+gs
+38.0068 1.41016 tr
+0 0 0 1 cmyk (G79) ArialMT 1.2 shgf
+gr
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+249 71.53 tr
+77.8285 ro
+0 0 m
+52.1728 0 li
+0 0 0 1 cmyk st
+gr
+gs
+1 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+433 167.53 tr
+gs
+1 0.447754 tr
+0 0 0 1 cmyk (G18) ArialMT 0.9 shgf
+gr
+gs
+1 -10.3522 tr
+0 0 0 1 cmyk (G18) ArialMT 0.9 shgf
+gr
+gs
+3.50049 -10.3522 tr
+0 0 0 1 cmyk (G85) ArialMT 0.9 shgf
+gr
+gs
+6.49756 -10.3522 tr
+0 0 0 1 cmyk (G72) ArialMT 0.9 shgf
+gr
+gs
+11.5029 -10.3522 tr
+0 0 0 1 cmyk (G86) ArialMT 0.9 shgf
+gr
+gs
+16.0029 -10.3522 tr
+0 0 0 1 cmyk (G82) ArialMT 0.9 shgf
+gr
+gs
+21.0083 -10.3522 tr
+0 0 0 1 cmyk (G88) ArialMT 0.9 shgf
+gr
+gs
+26.0137 -10.3522 tr
+0 0 0 1 cmyk (G85) ArialMT 0.9 shgf
+gr
+gs
+29.0107 -10.3522 tr
+0 0 0 1 cmyk (G70) ArialMT 0.9 shgf
+gr
+gs
+33.5107 -10.3522 tr
+0 0 0 1 cmyk (G72) ArialMT 0.9 shgf
+gr
+gs
+38.5161 -10.3522 tr
+0 0 0 1 cmyk (G20) ArialMT 0.9 shgf
+gr
+gs
+43.5215 -10.3522 tr
+0 0 0 1 cmyk (G18) ArialMT 0.9 shgf
+gr
+gs
+46.022 -10.3522 tr
+0 0 0 1 cmyk (G70) ArialMT 0.9 shgf
+gr
+gs
+50.522 -10.3522 tr
+0 0 0 1 cmyk (G82) ArialMT 0.9 shgf
+gr
+gs
+55.5273 -10.3522 tr
+0 0 0 1 cmyk (G81) ArialMT 0.9 shgf
+gr
+gs
+60.5327 -10.3522 tr
+0 0 0 1 cmyk (G87) ArialMT 0.9 shgf
+gr
+gs
+63.0332 -10.3522 tr
+0 0 0 1 cmyk (G68) ArialMT 0.9 shgf
+gr
+gs
+68.0386 -10.3522 tr
+0 0 0 1 cmyk (G70) ArialMT 0.9 shgf
+gr
+gs
+72.5386 -10.3522 tr
+0 0 0 1 cmyk (G87) ArialMT 0.9 shgf
+gr
+gs
+75.0391 -10.3522 tr
+0 0 0 1 cmyk (G86) ArialMT 0.9 shgf
+gr
+gs
+1 -21.1522 tr
+0 0 0 1 cmyk (G18) ArialMT 0.9 shgf
+gr
+gs
+3.50049 -21.1522 tr
+0 0 0 1 cmyk (G85) ArialMT 0.9 shgf
+gr
+gs
+6.49756 -21.1522 tr
+0 0 0 1 cmyk (G72) ArialMT 0.9 shgf
+gr
+gs
+11.5029 -21.1522 tr
+0 0 0 1 cmyk (G86) ArialMT 0.9 shgf
+gr
+gs
+16.0029 -21.1522 tr
+0 0 0 1 cmyk (G82) ArialMT 0.9 shgf
+gr
+gs
+21.0083 -21.1522 tr
+0 0 0 1 cmyk (G88) ArialMT 0.9 shgf
+gr
+gs
+26.0137 -21.1522 tr
+0 0 0 1 cmyk (G85) ArialMT 0.9 shgf
+gr
+gs
+29.0107 -21.1522 tr
+0 0 0 1 cmyk (G70) ArialMT 0.9 shgf
+gr
+gs
+33.5107 -21.1522 tr
+0 0 0 1 cmyk (G72) ArialMT 0.9 shgf
+gr
+gs
+38.5161 -21.1522 tr
+0 0 0 1 cmyk (G20) ArialMT 0.9 shgf
+gr
+gs
+43.5215 -21.1522 tr
+0 0 0 1 cmyk (G18) ArialMT 0.9 shgf
+gr
+gs
+46.022 -21.1522 tr
+0 0 0 1 cmyk (G72) ArialMT 0.9 shgf
+gr
+gs
+51.0273 -21.1522 tr
+0 0 0 1 cmyk (G89) ArialMT 0.9 shgf
+gr
+gs
+55.5273 -21.1522 tr
+0 0 0 1 cmyk (G72) ArialMT 0.9 shgf
+gr
+gs
+60.5327 -21.1522 tr
+0 0 0 1 cmyk (G81) ArialMT 0.9 shgf
+gr
+gs
+65.5381 -21.1522 tr
+0 0 0 1 cmyk (G87) ArialMT 0.9 shgf
+gr
+gs
+68.0386 -21.1522 tr
+0 0 0 1 cmyk (G86) ArialMT 0.9 shgf
+gr
+gs
+1 -31.9522 tr
+0 0 0 1 cmyk (G18) ArialMT 0.9 shgf
+gr
+gs
+3.50049 -31.9522 tr
+0 0 0 1 cmyk (G85) ArialMT 0.9 shgf
+gr
+gs
+6.49756 -31.9522 tr
+0 0 0 1 cmyk (G72) ArialMT 0.9 shgf
+gr
+gs
+11.5029 -31.9522 tr
+0 0 0 1 cmyk (G86) ArialMT 0.9 shgf
+gr
+gs
+16.0029 -31.9522 tr
+0 0 0 1 cmyk (G82) ArialMT 0.9 shgf
+gr
+gs
+21.0083 -31.9522 tr
+0 0 0 1 cmyk (G88) ArialMT 0.9 shgf
+gr
+gs
+26.0137 -31.9522 tr
+0 0 0 1 cmyk (G85) ArialMT 0.9 shgf
+gr
+gs
+29.0107 -31.9522 tr
+0 0 0 1 cmyk (G70) ArialMT 0.9 shgf
+gr
+gs
+33.5107 -31.9522 tr
+0 0 0 1 cmyk (G72) ArialMT 0.9 shgf
+gr
+gs
+38.5161 -31.9522 tr
+0 0 0 1 cmyk (G21) ArialMT 0.9 shgf
+gr
+gs
+1 -42.7522 tr
+0 0 0 1 cmyk (G18) ArialMT 0.9 shgf
+gr
+gs
+3.50049 -42.7522 tr
+0 0 0 1 cmyk (G86) ArialMT 0.9 shgf
+gr
+gs
+8.00049 -42.7522 tr
+0 0 0 1 cmyk (G72) ArialMT 0.9 shgf
+gr
+gs
+13.0059 -42.7522 tr
+0 0 0 1 cmyk (G68) ArialMT 0.9 shgf
+gr
+gs
+18.0112 -42.7522 tr
+0 0 0 1 cmyk (G85) ArialMT 0.9 shgf
+gr
+gs
+21.0083 -42.7522 tr
+0 0 0 1 cmyk (G70) ArialMT 0.9 shgf
+gr
+gs
+25.5083 -42.7522 tr
+0 0 0 1 cmyk (G75) ArialMT 0.9 shgf
+gr
+gr
+gs
+37.481 396.049 m
+37.481 396.049 193.443 396.049 193.443 396.049 curveto
+193.443 396.049 193.443 353.011 193.443 353.011 curveto
+193.443 353.011 37.481 353.011 37.481 353.011 curveto
+37.481 353.011 37.481 396.049 37.481 396.049 curveto
+cl
+eoclip newpath
+gs
+1.03793 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+38 395.53 tr
+154.924 -21 m
+154.924 -32.5984 120.245 -42 77.4621 -42 curveto
+34.681 -42 0 -32.5984 0 -21 curveto
+0 -9.40202 34.681 0 77.4621 0 curveto
+120.245 0 154.924 -9.40202 154.924 -21 curveto
+cl
+0.403922 0 0.137255 0.196078 cmyk eofill
+154.924 -21 m
+154.924 -32.5984 120.245 -42 77.4621 -42 curveto
+34.681 -42 0 -32.5984 0 -21 curveto
+0 -9.40202 34.681 0 77.4621 0 curveto
+120.245 0 154.924 -9.40202 154.924 -21 curveto
+cl
+0 0 0 1 cmyk st
+gr
+gs
+1.03793 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+61.669 382.53 tr
+gs
+1 1.26367 tr
+0 0 0 1 cmyk (G36) ArialMT 1.2 shgf
+gr
+gs
+9.00391 1.26367 tr
+0 0 0 1 cmyk (G74) ArialMT 1.2 shgf
+gr
+gs
+15.6777 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gs
+22.3516 1.26367 tr
+0 0 0 1 cmyk (G81) ArialMT 1.2 shgf
+gr
+gs
+29.0254 1.26367 tr
+0 0 0 1 cmyk (G87) ArialMT 1.2 shgf
+gr
+gs
+35.6934 1.26367 tr
+0 0 0 1 cmyk (G11) ArialMT 1.2 shgf
+gr
+gs
+39.6895 1.26367 tr
+0 0 0 1 cmyk (G54) ArialMT 1.2 shgf
+gr
+gs
+47.6934 1.26367 tr
+0 0 0 1 cmyk (G87) ArialMT 1.2 shgf
+gr
+gs
+51.0273 1.26367 tr
+0 0 0 1 cmyk (G85) ArialMT 1.2 shgf
+gr
+gs
+55.0234 1.26367 tr
+0 0 0 1 cmyk (G76) ArialMT 1.2 shgf
+gr
+gs
+57.6895 1.26367 tr
+0 0 0 1 cmyk (G74) ArialMT 1.2 shgf
+gr
+gs
+64.3633 1.26367 tr
+0 0 0 1 cmyk (G76) ArialMT 1.2 shgf
+gr
+gs
+70.3633 1.26367 tr
+0 0 0 1 cmyk (G73) ArialMT 1.2 shgf
+gr
+gs
+73.6973 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gs
+80.3711 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gs
+87.0449 1.26367 tr
+0 0 0 1 cmyk (G71) ArialMT 1.2 shgf
+gr
+gs
+93.7188 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gs
+100.393 1.26367 tr
+0 0 0 1 cmyk (G85) ArialMT 1.2 shgf
+gr
+gs
+104.389 1.26367 tr
+0 0 0 1 cmyk (G12) ArialMT 1.2 shgf
+gr
+gr
+gr
+gs
+24.557 326.049 m
+24.557 326.049 180.519 326.049 180.519 326.049 curveto
+180.519 326.049 180.519 283.011 180.519 283.011 curveto
+180.519 283.011 24.557 283.011 24.557 283.011 curveto
+24.557 283.011 24.557 326.049 24.557 326.049 curveto
+cl
+eoclip newpath
+gs
+1.03793 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+25.076 325.53 tr
+154.924 -21 m
+154.924 -32.5984 120.245 -42 77.4621 -42 curveto
+34.681 -42 0 -32.5984 0 -21 curveto
+0 -9.40202 34.681 0 77.4621 0 curveto
+120.245 0 154.924 -9.40202 154.924 -21 curveto
+cl
+0.403922 0 0.137255 0.196078 cmyk eofill
+154.924 -21 m
+154.924 -32.5984 120.245 -42 77.4621 -42 curveto
+34.681 -42 0 -32.5984 0 -21 curveto
+0 -9.40202 34.681 0 77.4621 0 curveto
+120.245 0 154.924 -9.40202 154.924 -21 curveto
+cl
+0 0 0 1 cmyk st
+gr
+gs
+1.03793 sw
+0 setlinecap
+0 setlinejoin
+[] 0 setdash
+36 313.53 tr
+gs
+1 1.26367 tr
+0 0 0 1 cmyk (G36) ArialMT 1.2 shgf
+gr
+gs
+9.00391 1.26367 tr
+0 0 0 1 cmyk (G74) ArialMT 1.2 shgf
+gr
+gs
+15.6777 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gs
+22.3516 1.26367 tr
+0 0 0 1 cmyk (G81) ArialMT 1.2 shgf
+gr
+gs
+29.0254 1.26367 tr
+0 0 0 1 cmyk (G87) ArialMT 1.2 shgf
+gr
+gs
+35.6934 1.26367 tr
+0 0 0 1 cmyk (G11) ArialMT 1.2 shgf
+gr
+gs
+39.6895 1.26367 tr
+0 0 0 1 cmyk (G49) ArialMT 1.2 shgf
+gr
+gs
+48.3555 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gs
+55.0293 1.26367 tr
+0 0 0 1 cmyk (G83) ArialMT 1.2 shgf
+gr
+gs
+61.7031 1.26367 tr
+0 0 0 1 cmyk (G82) ArialMT 1.2 shgf
+gr
+gs
+68.377 1.26367 tr
+0 0 0 1 cmyk (G80) ArialMT 1.2 shgf
+gr
+gs
+78.373 1.26367 tr
+0 0 0 1 cmyk (G88) ArialMT 1.2 shgf
+gr
+gs
+85.0469 1.26367 tr
+0 0 0 1 cmyk (G78) ArialMT 1.2 shgf
+gr
+gs
+94.3809 1.26367 tr
+0 0 0 1 cmyk (G73) ArialMT 1.2 shgf
+gr
+gs
+97.7148 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gs
+104.389 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gs
+111.062 1.26367 tr
+0 0 0 1 cmyk (G71) ArialMT 1.2 shgf
+gr
+gs
+117.736 1.26367 tr
+0 0 0 1 cmyk (G72) ArialMT 1.2 shgf
+gr
+gs
+124.41 1.26367 tr
+0 0 0 1 cmyk (G85) ArialMT 1.2 shgf
+gr
+gs
+128.406 1.26367 tr
+0 0 0 1 cmyk (G12) ArialMT 1.2 shgf
+gr
+gr
+gr
+%%PageTrailer
+restore
+sp
+%%Trailer
+end
+%%EOF
diff --git a/doc/pics/concept.png b/doc/pics/concept.png
new file mode 100644 (file)
index 0000000..12435f8
Binary files /dev/null and b/doc/pics/concept.png differ
diff --git a/doc/pics/concept.sla b/doc/pics/concept.sla
new file mode 100644 (file)
index 0000000..4d2fb51
--- /dev/null
@@ -0,0 +1,783 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SCRIBUSUTF8NEW Version="1.3.4" >
+ <DOCUMENT HalfRes="1" MAGMAX="800" TextPenShade="100" MAJGRID="100" ABSTSPALTEN="11" dispX="10" ScratchBottom="20" showBleed="1" AUTOCHECK="0" LANGUAGE="German" DPIn2="" DPgam="0" HCMS="0" UnderlineWidth="-1" TabFill="" DGAP="0" ORIENTATION="1" dispY="10" PASPECT="1" WIDTH="1" POLYR="0" SHOWLINK="0" MINWORDLEN="3" DPIn3="" UnderlinePos="-1" VTIEFSC="100" DOCLANGINFO="" COMMENTS="" BleedBottom="0" AutoSaveTime="600000" POLYS="0" GuideRad="10" rulerMode="1" TITLE="" KEYWORDS="" TabWidth="36" DSIZE="12" AUTOSPALTEN="1" PAGESIZE="A6" STIL="1" TextBackGroundShade="100" PEN="Black" POLYC="4" SnapToGuides="0" DISc="1" GROUPC="65" DOCFORMAT="" DOCDATE="" BORDERTOP="9" currentProfile="Postscript" MARGC="#0000ff" EndArrow="0" SHOWBASE="0" SHOWGRID="0" SnapToGrid="0" GUIDELOCK="0" StrikeThruPos="-1" WIDTHLINE="1" TextStrokeShade="100" SHOWLAYERM="0" DPuse="0" DPSo="0" DOCSOURCE="" FIRSTNUM="1" BleedRight="0" GuideC="#000080" BRUSH="Black" StartArrow="0" ScratchRight="100" POLYF="0.5" SHOWMARGIN="1" DIIm="0" DPbla="1" StrikeThruWidth="-1" VHOCHSC="100" DOCTYPE="" BORDERBOTTOM="9" BRUSHSHADE="100" StrokeText="Black" BASEGRID="12" VTIEF="33" DOCCONTRIB="" DOCRELATION="" PICTSCX="1" CPICT="White" PENLINE="Black" AutoSave="0" BASEO="0" DOCIDENT="" BOOK="0" constrain="15" PICTSCY="1" MAGSTEP="200" TextLineColor="None" ScratchTop="20" POLYFD="0" DPSFo="0" AUTOL="20" PUBLISHER="" BleedTop="0" ANZPAGES="1" PSCALE="1" LINESHADE="100" HYCOUNT="2" AUTHOR="" UNITS="0" BleedLeft="0" BORDERRIGHT="9" RANDF="0" MAJORC="#00ff00" PENSHADE="100" PENTEXT="Black" GRAB="4" showcolborders="0" SHOWGUIDES="1" DPInCMYK="" DPPr="" DPMo="" PAGEHEIGHT="297.64" PAGEWIDTH="419.53" BACKG="1" GuideZ="10" TextBackGround="None" MINGRID="20" VHOCH="33" DOCCOVER="" DCOL="1" EmbeddedPath="0" rulerYoffset="0" SHOWPICT="1" SHOWFRAME="1" AUTOMATIC="1" ALAYER="0" DOCRIGHTS="" PICTSHADE="100" ScratchLeft="100" rulerXoffset="0" showrulers="1" DPIn="" VKAPIT="75" DFONT="Bitstream Charter Bold Italic" BORDERLEFT="9" PAGEC="#ffffff" BaseC="#c0c0c0" MINORC="#00ff00" MAGMIN="10" STILLINE="1" TextLineShade="100" SHOWControl="0" >
+  <CheckProfile checkTransparency="1" autoCheck="1" maxResolution="4800" minResolution="72" checkOverflow="1" ignoreErrors="0" checkRasterPDF="1" checkResolution="1" checkGlyphs="1" Name="PDF 1.3" checkAnnotations="0" checkPictures="1" checkForGIF="1" ignoreOffLayers="0" checkOrphans="1" />
+  <CheckProfile checkTransparency="0" autoCheck="1" maxResolution="4800" minResolution="72" checkOverflow="1" ignoreErrors="0" checkRasterPDF="1" checkResolution="1" checkGlyphs="1" Name="PDF 1.4" checkAnnotations="0" checkPictures="1" checkForGIF="1" ignoreOffLayers="0" checkOrphans="1" />
+  <CheckProfile checkTransparency="1" autoCheck="1" maxResolution="4800" minResolution="144" checkOverflow="1" ignoreErrors="0" checkRasterPDF="1" checkResolution="1" checkGlyphs="1" Name="PDF/X-3" checkAnnotations="1" checkPictures="1" checkForGIF="1" ignoreOffLayers="0" checkOrphans="1" />
+  <CheckProfile checkTransparency="1" autoCheck="1" maxResolution="4800" minResolution="72" checkOverflow="1" ignoreErrors="0" checkRasterPDF="1" checkResolution="1" checkGlyphs="1" Name="PostScript" checkAnnotations="0" checkPictures="1" checkForGIF="1" ignoreOffLayers="0" checkOrphans="1" />
+  <CheckProfile checkTransparency="1" autoCheck="1" maxResolution="4800" minResolution="72" checkOverflow="1" ignoreErrors="0" checkRasterPDF="1" checkResolution="1" checkGlyphs="1" Name="Postscript" checkAnnotations="0" checkPictures="1" checkForGIF="1" ignoreOffLayers="0" checkOrphans="1" />
+  <COLOR Register="0" Spot="0" CMYK="#0f070000" NAME="AliceBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#000f2305" NAME="AntiqueWhite" />
+  <COLOR Register="0" Spot="0" CMYK="#00102400" NAME="AntiqueWhite1" />
+  <COLOR Register="0" Spot="0" CMYK="#000f2211" NAME="AntiqueWhite2" />
+  <COLOR Register="0" Spot="0" CMYK="#000d1d32" NAME="AntiqueWhite3" />
+  <COLOR Register="0" Spot="0" CMYK="#00081374" NAME="AntiqueWhite4" />
+  <COLOR Register="0" Spot="0" CMYK="#80002b00" NAME="Aquamarine" />
+  <COLOR Register="0" Spot="0" CMYK="#80002b00" NAME="Aquamarine1" />
+  <COLOR Register="0" Spot="0" CMYK="#78002811" NAME="Aquamarine2" />
+  <COLOR Register="0" Spot="0" CMYK="#67002332" NAME="Aquamarine3" />
+  <COLOR Register="0" Spot="0" CMYK="#46001774" NAME="Aquamarine4" />
+  <COLOR Register="0" Spot="0" CMYK="#0f000000" NAME="Azure" />
+  <COLOR Register="0" Spot="0" CMYK="#0f000000" NAME="Azure1" />
+  <COLOR Register="0" Spot="0" CMYK="#0e000011" NAME="Azure2" />
+  <COLOR Register="0" Spot="0" CMYK="#0c000032" NAME="Azure3" />
+  <COLOR Register="0" Spot="0" CMYK="#08000074" NAME="Azure4" />
+  <COLOR Register="0" Spot="0" CMYK="#0000190a" NAME="Beige" />
+  <COLOR Register="0" Spot="0" CMYK="#001b3b00" NAME="Bisque" />
+  <COLOR Register="0" Spot="0" CMYK="#001b3b00" NAME="Bisque1" />
+  <COLOR Register="0" Spot="0" CMYK="#00193711" NAME="Bisque2" />
+  <COLOR Register="0" Spot="0" CMYK="#00162f32" NAME="Bisque3" />
+  <COLOR Register="0" Spot="0" CMYK="#000e2074" NAME="Bisque4" />
+  <COLOR Register="0" Spot="0" CMYK="#000000ff" NAME="Black" />
+  <COLOR Register="0" Spot="0" CMYK="#00143200" NAME="BlanchedAlmond" />
+  <COLOR Register="0" Spot="0" CMYK="#ffff0000" NAME="Blue" />
+  <COLOR Register="0" Spot="0" CMYK="#ffff0000" NAME="Blue1" />
+  <COLOR Register="0" Spot="0" CMYK="#eeee0011" NAME="Blue2" />
+  <COLOR Register="0" Spot="0" CMYK="#cdcd0032" NAME="Blue3" />
+  <COLOR Register="0" Spot="0" CMYK="#8b8b0074" NAME="Blue4" />
+  <COLOR Register="0" Spot="0" CMYK="#58b7001d" NAME="BlueViolet" />
+  <COLOR Register="0" Spot="0" CMYK="#007b7b5a" NAME="Brown" />
+  <COLOR Register="0" Spot="0" CMYK="#00bfbf00" NAME="Brown1" />
+  <COLOR Register="0" Spot="0" CMYK="#00b3b311" NAME="Brown2" />
+  <COLOR Register="0" Spot="0" CMYK="#009a9a32" NAME="Brown3" />
+  <COLOR Register="0" Spot="0" CMYK="#00686874" NAME="Brown4" />
+  <COLOR Register="0" Spot="0" CMYK="#00265721" NAME="Burlywood" />
+  <COLOR Register="0" Spot="0" CMYK="#002c6400" NAME="Burlywood1" />
+  <COLOR Register="0" Spot="0" CMYK="#00295d11" NAME="Burlywood2" />
+  <COLOR Register="0" Spot="0" CMYK="#00235032" NAME="Burlywood3" />
+  <COLOR Register="0" Spot="0" CMYK="#00183674" NAME="Burlywood4" />
+  <COLOR Register="0" Spot="0" CMYK="#4102005f" NAME="CadetBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#670a0000" NAME="CadetBlue1" />
+  <COLOR Register="0" Spot="0" CMYK="#60090011" NAME="CadetBlue2" />
+  <COLOR Register="0" Spot="0" CMYK="#53080032" NAME="CadetBlue3" />
+  <COLOR Register="0" Spot="0" CMYK="#38050074" NAME="CadetBlue4" />
+  <COLOR Register="0" Spot="0" CMYK="#8000ff00" NAME="Chartreuse" />
+  <COLOR Register="0" Spot="0" CMYK="#8000ff00" NAME="Chartreuse1" />
+  <COLOR Register="0" Spot="0" CMYK="#7800ee11" NAME="Chartreuse2" />
+  <COLOR Register="0" Spot="0" CMYK="#6700cd32" NAME="Chartreuse3" />
+  <COLOR Register="0" Spot="0" CMYK="#46008b74" NAME="Chartreuse4" />
+  <COLOR Register="0" Spot="0" CMYK="#0069b42d" NAME="Chocolate" />
+  <COLOR Register="0" Spot="0" CMYK="#0080db00" NAME="Chocolate1" />
+  <COLOR Register="0" Spot="0" CMYK="#0078cd11" NAME="Chocolate2" />
+  <COLOR Register="0" Spot="0" CMYK="#0067b032" NAME="Chocolate3" />
+  <COLOR Register="0" Spot="0" CMYK="#00467874" NAME="Chocolate4" />
+  <COLOR Register="0" Spot="0" CMYK="#0080af00" NAME="Coral" />
+  <COLOR Register="0" Spot="0" CMYK="#008da900" NAME="Coral1" />
+  <COLOR Register="0" Spot="0" CMYK="#00849e11" NAME="Coral2" />
+  <COLOR Register="0" Spot="0" CMYK="#00728832" NAME="Coral3" />
+  <COLOR Register="0" Spot="0" CMYK="#004d5c74" NAME="Coral4" />
+  <COLOR Register="0" Spot="0" CMYK="#89580012" NAME="CornflowerBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#00072300" NAME="Cornsilk" />
+  <COLOR Register="0" Spot="0" CMYK="#00072300" NAME="Cornsilk1" />
+  <COLOR Register="0" Spot="0" CMYK="#00062111" NAME="Cornsilk2" />
+  <COLOR Register="0" Spot="0" CMYK="#00051c32" NAME="Cornsilk3" />
+  <COLOR Register="0" Spot="0" CMYK="#00031374" NAME="Cornsilk4" />
+  <COLOR Register="0" Spot="0" CMYK="#ff000000" NAME="Cyan" />
+  <COLOR Register="0" Spot="0" CMYK="#ff000000" NAME="Cyan1" />
+  <COLOR Register="0" Spot="0" CMYK="#ee000011" NAME="Cyan2" />
+  <COLOR Register="0" Spot="0" CMYK="#cd000032" NAME="Cyan3" />
+  <COLOR Register="0" Spot="0" CMYK="#8b000074" NAME="Cyan4" />
+  <COLOR Register="0" Spot="0" CMYK="#8b8b0074" NAME="DarkBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#8b000074" NAME="DarkCyan" />
+  <COLOR Register="0" Spot="0" CMYK="#0032ad47" NAME="DarkGoldenrod" />
+  <COLOR Register="0" Spot="0" CMYK="#0046f000" NAME="DarkGoldenrod1" />
+  <COLOR Register="0" Spot="0" CMYK="#0041e011" NAME="DarkGoldenrod2" />
+  <COLOR Register="0" Spot="0" CMYK="#0038c132" NAME="DarkGoldenrod3" />
+  <COLOR Register="0" Spot="0" CMYK="#00268374" NAME="DarkGoldenrod4" />
+  <COLOR Register="0" Spot="0" CMYK="#6400649b" NAME="DarkGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#00000056" NAME="DarkGrey" />
+  <COLOR Register="0" Spot="0" CMYK="#00065242" NAME="DarkKhaki" />
+  <COLOR Register="0" Spot="0" CMYK="#008b0074" NAME="DarkMagenta" />
+  <COLOR Register="0" Spot="0" CMYK="#16003c94" NAME="DarkOliveGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#35008f00" NAME="DarkOliveGreen1" />
+  <COLOR Register="0" Spot="0" CMYK="#32008611" NAME="DarkOliveGreen2" />
+  <COLOR Register="0" Spot="0" CMYK="#2b007332" NAME="DarkOliveGreen3" />
+  <COLOR Register="0" Spot="0" CMYK="#1d004e74" NAME="DarkOliveGreen4" />
+  <COLOR Register="0" Spot="0" CMYK="#0073ff00" NAME="DarkOrange" />
+  <COLOR Register="0" Spot="0" CMYK="#0080ff00" NAME="DarkOrange1" />
+  <COLOR Register="0" Spot="0" CMYK="#0078ee11" NAME="DarkOrange2" />
+  <COLOR Register="0" Spot="0" CMYK="#0067cd32" NAME="DarkOrange3" />
+  <COLOR Register="0" Spot="0" CMYK="#00468b74" NAME="DarkOrange4" />
+  <COLOR Register="0" Spot="0" CMYK="#339a0033" NAME="DarkOrchid" />
+  <COLOR Register="0" Spot="0" CMYK="#40c10000" NAME="DarkOrchid1" />
+  <COLOR Register="0" Spot="0" CMYK="#3cb40011" NAME="DarkOrchid2" />
+  <COLOR Register="0" Spot="0" CMYK="#339b0032" NAME="DarkOrchid3" />
+  <COLOR Register="0" Spot="0" CMYK="#23690074" NAME="DarkOrchid4" />
+  <COLOR Register="0" Spot="0" CMYK="#008b8b74" NAME="DarkRed" />
+  <COLOR Register="0" Spot="0" CMYK="#00536f16" NAME="DarkSalmon" />
+  <COLOR Register="0" Spot="0" CMYK="#2d002d43" NAME="DarkSeaGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#3e003e00" NAME="DarkSeaGreen1" />
+  <COLOR Register="0" Spot="0" CMYK="#3a003a11" NAME="DarkSeaGreen2" />
+  <COLOR Register="0" Spot="0" CMYK="#32003232" NAME="DarkSeaGreen3" />
+  <COLOR Register="0" Spot="0" CMYK="#22002274" NAME="DarkSeaGreen4" />
+  <COLOR Register="0" Spot="0" CMYK="#434e0074" NAME="DarkSlateBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#200000b0" NAME="DarkSlateGrey" />
+  <COLOR Register="0" Spot="0" CMYK="#68000000" NAME="DarkSlateGrey1" />
+  <COLOR Register="0" Spot="0" CMYK="#61000011" NAME="DarkSlateGrey2" />
+  <COLOR Register="0" Spot="0" CMYK="#54000032" NAME="DarkSlateGrey3" />
+  <COLOR Register="0" Spot="0" CMYK="#39000074" NAME="DarkSlateGrey4" />
+  <COLOR Register="0" Spot="0" CMYK="#d103002e" NAME="DarkTurquoise" />
+  <COLOR Register="0" Spot="0" CMYK="#3fd3002c" NAME="DarkViolet" />
+  <COLOR Register="0" Spot="0" CMYK="#00eb6c00" NAME="DeepPink" />
+  <COLOR Register="0" Spot="0" CMYK="#00eb6c00" NAME="DeepPink1" />
+  <COLOR Register="0" Spot="0" CMYK="#00dc6511" NAME="DeepPink2" />
+  <COLOR Register="0" Spot="0" CMYK="#00bd5732" NAME="DeepPink3" />
+  <COLOR Register="0" Spot="0" CMYK="#00813b74" NAME="DeepPink4" />
+  <COLOR Register="0" Spot="0" CMYK="#ff400000" NAME="DeepSkyBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#ff400000" NAME="DeepSkyBlue1" />
+  <COLOR Register="0" Spot="0" CMYK="#ee3c0011" NAME="DeepSkyBlue2" />
+  <COLOR Register="0" Spot="0" CMYK="#cd330032" NAME="DeepSkyBlue3" />
+  <COLOR Register="0" Spot="0" CMYK="#8b230074" NAME="DeepSkyBlue4" />
+  <COLOR Register="0" Spot="0" CMYK="#00000096" NAME="DimGrey" />
+  <COLOR Register="0" Spot="0" CMYK="#e16f0000" NAME="DodgerBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#e16f0000" NAME="DodgerBlue1" />
+  <COLOR Register="0" Spot="0" CMYK="#d2680011" NAME="DodgerBlue2" />
+  <COLOR Register="0" Spot="0" CMYK="#b5590032" NAME="DodgerBlue3" />
+  <COLOR Register="0" Spot="0" CMYK="#7b3d0074" NAME="DodgerBlue4" />
+  <COLOR Register="0" Spot="0" CMYK="#0090904d" NAME="Firebrick" />
+  <COLOR Register="0" Spot="0" CMYK="#00cfcf00" NAME="Firebrick1" />
+  <COLOR Register="0" Spot="0" CMYK="#00c2c211" NAME="Firebrick2" />
+  <COLOR Register="0" Spot="0" CMYK="#00a7a732" NAME="Firebrick3" />
+  <COLOR Register="0" Spot="0" CMYK="#00717174" NAME="Firebrick4" />
+  <COLOR Register="0" Spot="0" CMYK="#00050f00" NAME="FloralWhite" />
+  <COLOR Register="0" Spot="0" CMYK="#69006974" NAME="ForestGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#00000023" NAME="Gainsboro" />
+  <COLOR Register="0" Spot="0" CMYK="#07070000" NAME="GhostWhite" />
+  <COLOR Register="0" Spot="0" CMYK="#0028ff00" NAME="Gold" />
+  <COLOR Register="0" Spot="0" CMYK="#0028ff00" NAME="Gold1" />
+  <COLOR Register="0" Spot="0" CMYK="#0025ee11" NAME="Gold2" />
+  <COLOR Register="0" Spot="0" CMYK="#0020cd32" NAME="Gold3" />
+  <COLOR Register="0" Spot="0" CMYK="#00168b74" NAME="Gold4" />
+  <COLOR Register="0" Spot="0" CMYK="#0035ba25" NAME="Goldenrod" />
+  <COLOR Register="0" Spot="0" CMYK="#003eda00" NAME="Goldenrod1" />
+  <COLOR Register="0" Spot="0" CMYK="#003acc11" NAME="Goldenrod2" />
+  <COLOR Register="0" Spot="0" CMYK="#0032b032" NAME="Goldenrod3" />
+  <COLOR Register="0" Spot="0" CMYK="#00227774" NAME="Goldenrod4" />
+  <COLOR Register="0" Spot="0" CMYK="#ff00ff00" NAME="Green" />
+  <COLOR Register="0" Spot="0" CMYK="#ff00ff00" NAME="Green1" />
+  <COLOR Register="0" Spot="0" CMYK="#ee00ee11" NAME="Green2" />
+  <COLOR Register="0" Spot="0" CMYK="#cd00cd32" NAME="Green3" />
+  <COLOR Register="0" Spot="0" CMYK="#8b008b74" NAME="Green4" />
+  <COLOR Register="0" Spot="0" CMYK="#5200d000" NAME="GreenYellow" />
+  <COLOR Register="0" Spot="0" CMYK="#00000041" NAME="Grey" />
+  <COLOR Register="0" Spot="0" CMYK="#000000ff" NAME="Grey0" />
+  <COLOR Register="0" Spot="0" CMYK="#000000fc" NAME="Grey1" />
+  <COLOR Register="0" Spot="0" CMYK="#000000e5" NAME="Grey10" />
+  <COLOR Register="0" Spot="0" CMYK="#00000000" NAME="Grey100" />
+  <COLOR Register="0" Spot="0" CMYK="#000000e3" NAME="Grey11" />
+  <COLOR Register="0" Spot="0" CMYK="#000000e0" NAME="Grey12" />
+  <COLOR Register="0" Spot="0" CMYK="#000000de" NAME="Grey13" />
+  <COLOR Register="0" Spot="0" CMYK="#000000db" NAME="Grey14" />
+  <COLOR Register="0" Spot="0" CMYK="#000000d9" NAME="Grey15" />
+  <COLOR Register="0" Spot="0" CMYK="#000000d6" NAME="Grey16" />
+  <COLOR Register="0" Spot="0" CMYK="#000000d4" NAME="Grey17" />
+  <COLOR Register="0" Spot="0" CMYK="#000000d1" NAME="Grey18" />
+  <COLOR Register="0" Spot="0" CMYK="#000000cf" NAME="Grey19" />
+  <COLOR Register="0" Spot="0" CMYK="#000000fa" NAME="Grey2" />
+  <COLOR Register="0" Spot="0" CMYK="#000000cc" NAME="Grey20" />
+  <COLOR Register="0" Spot="0" CMYK="#000000c9" NAME="Grey21" />
+  <COLOR Register="0" Spot="0" CMYK="#000000c7" NAME="Grey22" />
+  <COLOR Register="0" Spot="0" CMYK="#000000c4" NAME="Grey23" />
+  <COLOR Register="0" Spot="0" CMYK="#000000c2" NAME="Grey24" />
+  <COLOR Register="0" Spot="0" CMYK="#000000bf" NAME="Grey25" />
+  <COLOR Register="0" Spot="0" CMYK="#000000bd" NAME="Grey26" />
+  <COLOR Register="0" Spot="0" CMYK="#000000ba" NAME="Grey27" />
+  <COLOR Register="0" Spot="0" CMYK="#000000b8" NAME="Grey28" />
+  <COLOR Register="0" Spot="0" CMYK="#000000b5" NAME="Grey29" />
+  <COLOR Register="0" Spot="0" CMYK="#000000f7" NAME="Grey3" />
+  <COLOR Register="0" Spot="0" CMYK="#000000b2" NAME="Grey30" />
+  <COLOR Register="0" Spot="0" CMYK="#000000b0" NAME="Grey31" />
+  <COLOR Register="0" Spot="0" CMYK="#000000ad" NAME="Grey32" />
+  <COLOR Register="0" Spot="0" CMYK="#000000ab" NAME="Grey33" />
+  <COLOR Register="0" Spot="0" CMYK="#000000a8" NAME="Grey34" />
+  <COLOR Register="0" Spot="0" CMYK="#000000a6" NAME="Grey35" />
+  <COLOR Register="0" Spot="0" CMYK="#000000a3" NAME="Grey36" />
+  <COLOR Register="0" Spot="0" CMYK="#000000a1" NAME="Grey37" />
+  <COLOR Register="0" Spot="0" CMYK="#0000009e" NAME="Grey38" />
+  <COLOR Register="0" Spot="0" CMYK="#0000009c" NAME="Grey39" />
+  <COLOR Register="0" Spot="0" CMYK="#000000f5" NAME="Grey4" />
+  <COLOR Register="0" Spot="0" CMYK="#00000099" NAME="Grey40" />
+  <COLOR Register="0" Spot="0" CMYK="#00000096" NAME="Grey41" />
+  <COLOR Register="0" Spot="0" CMYK="#00000094" NAME="Grey42" />
+  <COLOR Register="0" Spot="0" CMYK="#00000091" NAME="Grey43" />
+  <COLOR Register="0" Spot="0" CMYK="#0000008f" NAME="Grey44" />
+  <COLOR Register="0" Spot="0" CMYK="#0000008c" NAME="Grey45" />
+  <COLOR Register="0" Spot="0" CMYK="#0000008a" NAME="Grey46" />
+  <COLOR Register="0" Spot="0" CMYK="#00000087" NAME="Grey47" />
+  <COLOR Register="0" Spot="0" CMYK="#00000085" NAME="Grey48" />
+  <COLOR Register="0" Spot="0" CMYK="#00000082" NAME="Grey49" />
+  <COLOR Register="0" Spot="0" CMYK="#000000f2" NAME="Grey5" />
+  <COLOR Register="0" Spot="0" CMYK="#00000080" NAME="Grey50" />
+  <COLOR Register="0" Spot="0" CMYK="#0000007d" NAME="Grey51" />
+  <COLOR Register="0" Spot="0" CMYK="#0000007a" NAME="Grey52" />
+  <COLOR Register="0" Spot="0" CMYK="#00000078" NAME="Grey53" />
+  <COLOR Register="0" Spot="0" CMYK="#00000075" NAME="Grey54" />
+  <COLOR Register="0" Spot="0" CMYK="#00000073" NAME="Grey55" />
+  <COLOR Register="0" Spot="0" CMYK="#00000070" NAME="Grey56" />
+  <COLOR Register="0" Spot="0" CMYK="#0000006e" NAME="Grey57" />
+  <COLOR Register="0" Spot="0" CMYK="#0000006b" NAME="Grey58" />
+  <COLOR Register="0" Spot="0" CMYK="#00000069" NAME="Grey59" />
+  <COLOR Register="0" Spot="0" CMYK="#000000f0" NAME="Grey6" />
+  <COLOR Register="0" Spot="0" CMYK="#00000066" NAME="Grey60" />
+  <COLOR Register="0" Spot="0" CMYK="#00000063" NAME="Grey61" />
+  <COLOR Register="0" Spot="0" CMYK="#00000061" NAME="Grey62" />
+  <COLOR Register="0" Spot="0" CMYK="#0000005e" NAME="Grey63" />
+  <COLOR Register="0" Spot="0" CMYK="#0000005c" NAME="Grey64" />
+  <COLOR Register="0" Spot="0" CMYK="#00000059" NAME="Grey65" />
+  <COLOR Register="0" Spot="0" CMYK="#00000057" NAME="Grey66" />
+  <COLOR Register="0" Spot="0" CMYK="#00000054" NAME="Grey67" />
+  <COLOR Register="0" Spot="0" CMYK="#00000052" NAME="Grey68" />
+  <COLOR Register="0" Spot="0" CMYK="#0000004f" NAME="Grey69" />
+  <COLOR Register="0" Spot="0" CMYK="#000000ed" NAME="Grey7" />
+  <COLOR Register="0" Spot="0" CMYK="#0000004c" NAME="Grey70" />
+  <COLOR Register="0" Spot="0" CMYK="#0000004a" NAME="Grey71" />
+  <COLOR Register="0" Spot="0" CMYK="#00000047" NAME="Grey72" />
+  <COLOR Register="0" Spot="0" CMYK="#00000045" NAME="Grey73" />
+  <COLOR Register="0" Spot="0" CMYK="#00000042" NAME="Grey74" />
+  <COLOR Register="0" Spot="0" CMYK="#00000040" NAME="Grey75" />
+  <COLOR Register="0" Spot="0" CMYK="#0000003d" NAME="Grey76" />
+  <COLOR Register="0" Spot="0" CMYK="#0000003b" NAME="Grey77" />
+  <COLOR Register="0" Spot="0" CMYK="#00000038" NAME="Grey78" />
+  <COLOR Register="0" Spot="0" CMYK="#00000036" NAME="Grey79" />
+  <COLOR Register="0" Spot="0" CMYK="#000000eb" NAME="Grey8" />
+  <COLOR Register="0" Spot="0" CMYK="#00000033" NAME="Grey80" />
+  <COLOR Register="0" Spot="0" CMYK="#00000030" NAME="Grey81" />
+  <COLOR Register="0" Spot="0" CMYK="#0000002e" NAME="Grey82" />
+  <COLOR Register="0" Spot="0" CMYK="#0000002b" NAME="Grey83" />
+  <COLOR Register="0" Spot="0" CMYK="#00000029" NAME="Grey84" />
+  <COLOR Register="0" Spot="0" CMYK="#00000026" NAME="Grey85" />
+  <COLOR Register="0" Spot="0" CMYK="#00000024" NAME="Grey86" />
+  <COLOR Register="0" Spot="0" CMYK="#00000021" NAME="Grey87" />
+  <COLOR Register="0" Spot="0" CMYK="#0000001f" NAME="Grey88" />
+  <COLOR Register="0" Spot="0" CMYK="#0000001c" NAME="Grey89" />
+  <COLOR Register="0" Spot="0" CMYK="#000000e8" NAME="Grey9" />
+  <COLOR Register="0" Spot="0" CMYK="#0000001a" NAME="Grey90" />
+  <COLOR Register="0" Spot="0" CMYK="#00000017" NAME="Grey91" />
+  <COLOR Register="0" Spot="0" CMYK="#00000014" NAME="Grey92" />
+  <COLOR Register="0" Spot="0" CMYK="#00000012" NAME="Grey93" />
+  <COLOR Register="0" Spot="0" CMYK="#0000000f" NAME="Grey94" />
+  <COLOR Register="0" Spot="0" CMYK="#0000000d" NAME="Grey95" />
+  <COLOR Register="0" Spot="0" CMYK="#0000000a" NAME="Grey96" />
+  <COLOR Register="0" Spot="0" CMYK="#00000008" NAME="Grey97" />
+  <COLOR Register="0" Spot="0" CMYK="#00000005" NAME="Grey98" />
+  <COLOR Register="0" Spot="0" CMYK="#00000003" NAME="Grey99" />
+  <COLOR Register="0" Spot="0" CMYK="#0f000f00" NAME="Honeydew" />
+  <COLOR Register="0" Spot="0" CMYK="#0f000f00" NAME="Honeydew1" />
+  <COLOR Register="0" Spot="0" CMYK="#0e000e11" NAME="Honeydew2" />
+  <COLOR Register="0" Spot="0" CMYK="#0c000c32" NAME="Honeydew3" />
+  <COLOR Register="0" Spot="0" CMYK="#08000874" NAME="Honeydew4" />
+  <COLOR Register="0" Spot="0" CMYK="#00964b00" NAME="HotPink" />
+  <COLOR Register="0" Spot="0" CMYK="#00914b00" NAME="HotPink1" />
+  <COLOR Register="0" Spot="0" CMYK="#00844711" NAME="HotPink2" />
+  <COLOR Register="0" Spot="0" CMYK="#006d3d32" NAME="HotPink3" />
+  <COLOR Register="0" Spot="0" CMYK="#00512974" NAME="HotPink4" />
+  <COLOR Register="0" Spot="0" CMYK="#00717132" NAME="IndianRed" />
+  <COLOR Register="0" Spot="0" CMYK="#00959500" NAME="IndianRed1" />
+  <COLOR Register="0" Spot="0" CMYK="#008b8b11" NAME="IndianRed2" />
+  <COLOR Register="0" Spot="0" CMYK="#00787832" NAME="IndianRed3" />
+  <COLOR Register="0" Spot="0" CMYK="#00515174" NAME="IndianRed4" />
+  <COLOR Register="0" Spot="0" CMYK="#00000f00" NAME="Ivory" />
+  <COLOR Register="0" Spot="0" CMYK="#00000f00" NAME="Ivory1" />
+  <COLOR Register="0" Spot="0" CMYK="#00000e11" NAME="Ivory2" />
+  <COLOR Register="0" Spot="0" CMYK="#00000c32" NAME="Ivory3" />
+  <COLOR Register="0" Spot="0" CMYK="#00000874" NAME="Ivory4" />
+  <COLOR Register="0" Spot="0" CMYK="#000a640f" NAME="Khaki" />
+  <COLOR Register="0" Spot="0" CMYK="#00097000" NAME="Khaki1" />
+  <COLOR Register="0" Spot="0" CMYK="#00086911" NAME="Khaki2" />
+  <COLOR Register="0" Spot="0" CMYK="#00075a32" NAME="Khaki3" />
+  <COLOR Register="0" Spot="0" CMYK="#00053d74" NAME="Khaki4" />
+  <COLOR Register="0" Spot="0" CMYK="#14140005" NAME="Lavender" />
+  <COLOR Register="0" Spot="0" CMYK="#000f0a00" NAME="LavenderBlush" />
+  <COLOR Register="0" Spot="0" CMYK="#000f0a00" NAME="LavenderBlush1" />
+  <COLOR Register="0" Spot="0" CMYK="#000e0911" NAME="LavenderBlush2" />
+  <COLOR Register="0" Spot="0" CMYK="#000c0832" NAME="LavenderBlush3" />
+  <COLOR Register="0" Spot="0" CMYK="#00080574" NAME="LavenderBlush4" />
+  <COLOR Register="0" Spot="0" CMYK="#8000fc03" NAME="LawnGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#00053200" NAME="LemonChiffon" />
+  <COLOR Register="0" Spot="0" CMYK="#00053200" NAME="LemonChiffon1" />
+  <COLOR Register="0" Spot="0" CMYK="#00052f11" NAME="LemonChiffon2" />
+  <COLOR Register="0" Spot="0" CMYK="#00042832" NAME="LemonChiffon3" />
+  <COLOR Register="0" Spot="0" CMYK="#00021b74" NAME="LemonChiffon4" />
+  <COLOR Register="0" Spot="0" CMYK="#390e0019" NAME="LightBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#40100000" NAME="LightBlue1" />
+  <COLOR Register="0" Spot="0" CMYK="#3c0f0011" NAME="LightBlue2" />
+  <COLOR Register="0" Spot="0" CMYK="#330d0032" NAME="LightBlue3" />
+  <COLOR Register="0" Spot="0" CMYK="#23080074" NAME="LightBlue4" />
+  <COLOR Register="0" Spot="0" CMYK="#0070700f" NAME="LightCoral" />
+  <COLOR Register="0" Spot="0" CMYK="#1f000000" NAME="LightCyan" />
+  <COLOR Register="0" Spot="0" CMYK="#1f000000" NAME="LightCyan1" />
+  <COLOR Register="0" Spot="0" CMYK="#1d000011" NAME="LightCyan2" />
+  <COLOR Register="0" Spot="0" CMYK="#19000032" NAME="LightCyan3" />
+  <COLOR Register="0" Spot="0" CMYK="#11000074" NAME="LightCyan4" />
+  <COLOR Register="0" Spot="0" CMYK="#00116c11" NAME="LightGoldenrod" />
+  <COLOR Register="0" Spot="0" CMYK="#00137400" NAME="LightGoldenrod1" />
+  <COLOR Register="0" Spot="0" CMYK="#00126c11" NAME="LightGoldenrod2" />
+  <COLOR Register="0" Spot="0" CMYK="#000f5d32" NAME="LightGoldenrod3" />
+  <COLOR Register="0" Spot="0" CMYK="#000a3f74" NAME="LightGoldenrod4" />
+  <COLOR Register="0" Spot="0" CMYK="#00002805" NAME="LightGoldenrodYellow" />
+  <COLOR Register="0" Spot="0" CMYK="#5e005e11" NAME="LightGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#0000002c" NAME="LightGrey" />
+  <COLOR Register="0" Spot="0" CMYK="#00493e00" NAME="LightPink" />
+  <COLOR Register="0" Spot="0" CMYK="#00514600" NAME="LightPink1" />
+  <COLOR Register="0" Spot="0" CMYK="#004c4111" NAME="LightPink2" />
+  <COLOR Register="0" Spot="0" CMYK="#00413832" NAME="LightPink3" />
+  <COLOR Register="0" Spot="0" CMYK="#002c2674" NAME="LightPink4" />
+  <COLOR Register="0" Spot="0" CMYK="#005f8500" NAME="LightSalmon" />
+  <COLOR Register="0" Spot="0" CMYK="#005f8500" NAME="LightSalmon1" />
+  <COLOR Register="0" Spot="0" CMYK="#00597c11" NAME="LightSalmon2" />
+  <COLOR Register="0" Spot="0" CMYK="#004c6b32" NAME="LightSalmon3" />
+  <COLOR Register="0" Spot="0" CMYK="#00344974" NAME="LightSalmon4" />
+  <COLOR Register="0" Spot="0" CMYK="#9200084d" NAME="LightSeaGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#732c0005" NAME="LightSkyBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#4f1d0000" NAME="LightSkyBlue1" />
+  <COLOR Register="0" Spot="0" CMYK="#4a1b0011" NAME="LightSkyBlue2" />
+  <COLOR Register="0" Spot="0" CMYK="#40170032" NAME="LightSkyBlue3" />
+  <COLOR Register="0" Spot="0" CMYK="#2b100074" NAME="LightSkyBlue4" />
+  <COLOR Register="0" Spot="0" CMYK="#7b8f0000" NAME="LightSlateBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#22110066" NAME="LightSlateGrey" />
+  <COLOR Register="0" Spot="0" CMYK="#2e1a0021" NAME="LightSteelBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#351e0000" NAME="LightSteelBlue1" />
+  <COLOR Register="0" Spot="0" CMYK="#321c0011" NAME="LightSteelBlue2" />
+  <COLOR Register="0" Spot="0" CMYK="#2b180032" NAME="LightSteelBlue3" />
+  <COLOR Register="0" Spot="0" CMYK="#1d100074" NAME="LightSteelBlue4" />
+  <COLOR Register="0" Spot="0" CMYK="#00001f00" NAME="LightYellow" />
+  <COLOR Register="0" Spot="0" CMYK="#00001f00" NAME="LightYellow1" />
+  <COLOR Register="0" Spot="0" CMYK="#00001d11" NAME="LightYellow2" />
+  <COLOR Register="0" Spot="0" CMYK="#00001932" NAME="LightYellow3" />
+  <COLOR Register="0" Spot="0" CMYK="#00001174" NAME="LightYellow4" />
+  <COLOR Register="0" Spot="0" CMYK="#9b009b32" NAME="LimeGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#000a1405" NAME="Linen" />
+  <COLOR Register="0" Spot="0" CMYK="#00ff0000" NAME="Magenta" />
+  <COLOR Register="0" Spot="0" CMYK="#00ff0000" NAME="Magenta1" />
+  <COLOR Register="0" Spot="0" CMYK="#00ee0011" NAME="Magenta2" />
+  <COLOR Register="0" Spot="0" CMYK="#00cd0032" NAME="Magenta3" />
+  <COLOR Register="0" Spot="0" CMYK="#008b0074" NAME="Magenta4" />
+  <COLOR Register="0" Spot="0" CMYK="#0080504f" NAME="Maroon" />
+  <COLOR Register="0" Spot="0" CMYK="#00cb4c00" NAME="Maroon1" />
+  <COLOR Register="0" Spot="0" CMYK="#00be4711" NAME="Maroon2" />
+  <COLOR Register="0" Spot="0" CMYK="#00a43d32" NAME="Maroon3" />
+  <COLOR Register="0" Spot="0" CMYK="#006f2974" NAME="Maroon4" />
+  <COLOR Register="0" Spot="0" CMYK="#67002332" NAME="MediumAquamarine" />
+  <COLOR Register="0" Spot="0" CMYK="#cdcd0032" NAME="MediumBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#197e002c" NAME="MediumOrchid" />
+  <COLOR Register="0" Spot="0" CMYK="#1f990000" NAME="MediumOrchid1" />
+  <COLOR Register="0" Spot="0" CMYK="#1d8f0011" NAME="MediumOrchid2" />
+  <COLOR Register="0" Spot="0" CMYK="#197b0032" NAME="MediumOrchid3" />
+  <COLOR Register="0" Spot="0" CMYK="#11540074" NAME="MediumOrchid4" />
+  <COLOR Register="0" Spot="0" CMYK="#486b0024" NAME="MediumPurple" />
+  <COLOR Register="0" Spot="0" CMYK="#547d0000" NAME="MediumPurple1" />
+  <COLOR Register="0" Spot="0" CMYK="#4f750011" NAME="MediumPurple2" />
+  <COLOR Register="0" Spot="0" CMYK="#44650032" NAME="MediumPurple3" />
+  <COLOR Register="0" Spot="0" CMYK="#2e440074" NAME="MediumPurple4" />
+  <COLOR Register="0" Spot="0" CMYK="#7700424c" NAME="MediumSeaGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#73860011" NAME="MediumSlateBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#fa006005" NAME="MediumSpringGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#8900052e" NAME="MediumTurquoise" />
+  <COLOR Register="0" Spot="0" CMYK="#00b24238" NAME="MediumVioletRed" />
+  <COLOR Register="0" Spot="0" CMYK="#5757008f" NAME="MidnightBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#0a000500" NAME="MintCream" />
+  <COLOR Register="0" Spot="0" CMYK="#001b1e00" NAME="MistyRose" />
+  <COLOR Register="0" Spot="0" CMYK="#001b1e00" NAME="MistyRose1" />
+  <COLOR Register="0" Spot="0" CMYK="#00191c11" NAME="MistyRose2" />
+  <COLOR Register="0" Spot="0" CMYK="#00161832" NAME="MistyRose3" />
+  <COLOR Register="0" Spot="0" CMYK="#000e1074" NAME="MistyRose4" />
+  <COLOR Register="0" Spot="0" CMYK="#001b4a00" NAME="Moccasin" />
+  <COLOR Register="0" Spot="0" CMYK="#00215200" NAME="NavajoWhite" />
+  <COLOR Register="0" Spot="0" CMYK="#00215200" NAME="NavajoWhite1" />
+  <COLOR Register="0" Spot="0" CMYK="#001f4d11" NAME="NavajoWhite2" />
+  <COLOR Register="0" Spot="0" CMYK="#001a4232" NAME="NavajoWhite3" />
+  <COLOR Register="0" Spot="0" CMYK="#00122d74" NAME="NavajoWhite4" />
+  <COLOR Register="0" Spot="0" CMYK="#8080007f" NAME="NavyBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#00081702" NAME="OldLace" />
+  <COLOR Register="0" Spot="0" CMYK="#23006b71" NAME="OliveDrab" />
+  <COLOR Register="0" Spot="0" CMYK="#3f00c100" NAME="OliveDrab1" />
+  <COLOR Register="0" Spot="0" CMYK="#3b00b411" NAME="OliveDrab2" />
+  <COLOR Register="0" Spot="0" CMYK="#33009b32" NAME="OliveDrab3" />
+  <COLOR Register="0" Spot="0" CMYK="#22006974" NAME="OliveDrab4" />
+  <COLOR Register="0" Spot="0" CMYK="#005aff00" NAME="Orange" />
+  <COLOR Register="0" Spot="0" CMYK="#005aff00" NAME="Orange1" />
+  <COLOR Register="0" Spot="0" CMYK="#0054ee11" NAME="Orange2" />
+  <COLOR Register="0" Spot="0" CMYK="#0048cd32" NAME="Orange3" />
+  <COLOR Register="0" Spot="0" CMYK="#00318b74" NAME="Orange4" />
+  <COLOR Register="0" Spot="0" CMYK="#00baff00" NAME="OrangeRed" />
+  <COLOR Register="0" Spot="0" CMYK="#00baff00" NAME="OrangeRed1" />
+  <COLOR Register="0" Spot="0" CMYK="#00aeee11" NAME="OrangeRed2" />
+  <COLOR Register="0" Spot="0" CMYK="#0096cd32" NAME="OrangeRed3" />
+  <COLOR Register="0" Spot="0" CMYK="#00668b74" NAME="OrangeRed4" />
+  <COLOR Register="0" Spot="0" CMYK="#006a0425" NAME="Orchid" />
+  <COLOR Register="0" Spot="0" CMYK="#007c0500" NAME="Orchid1" />
+  <COLOR Register="0" Spot="0" CMYK="#00740511" NAME="Orchid2" />
+  <COLOR Register="0" Spot="0" CMYK="#00640432" NAME="Orchid3" />
+  <COLOR Register="0" Spot="0" CMYK="#00440274" NAME="Orchid4" />
+  <COLOR Register="0" Spot="0" CMYK="#00064411" NAME="PaleGoldenrod" />
+  <COLOR Register="0" Spot="0" CMYK="#63006304" NAME="PaleGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#65006500" NAME="PaleGreen1" />
+  <COLOR Register="0" Spot="0" CMYK="#5e005e11" NAME="PaleGreen2" />
+  <COLOR Register="0" Spot="0" CMYK="#51005132" NAME="PaleGreen3" />
+  <COLOR Register="0" Spot="0" CMYK="#37003774" NAME="PaleGreen4" />
+  <COLOR Register="0" Spot="0" CMYK="#3f000011" NAME="PaleTurquoise" />
+  <COLOR Register="0" Spot="0" CMYK="#44000000" NAME="PaleTurquoise1" />
+  <COLOR Register="0" Spot="0" CMYK="#40000011" NAME="PaleTurquoise2" />
+  <COLOR Register="0" Spot="0" CMYK="#37000032" NAME="PaleTurquoise3" />
+  <COLOR Register="0" Spot="0" CMYK="#25000074" NAME="PaleTurquoise4" />
+  <COLOR Register="0" Spot="0" CMYK="#006b4824" NAME="PaleVioletRed" />
+  <COLOR Register="0" Spot="0" CMYK="#007d5400" NAME="PaleVioletRed1" />
+  <COLOR Register="0" Spot="0" CMYK="#00754f11" NAME="PaleVioletRed2" />
+  <COLOR Register="0" Spot="0" CMYK="#00654432" NAME="PaleVioletRed3" />
+  <COLOR Register="0" Spot="0" CMYK="#00442e74" NAME="PaleVioletRed4" />
+  <COLOR Register="0" Spot="0" CMYK="#00102a00" NAME="PapayaWhip" />
+  <COLOR Register="0" Spot="0" CMYK="#00254600" NAME="PeachPuff" />
+  <COLOR Register="0" Spot="0" CMYK="#00254600" NAME="PeachPuff1" />
+  <COLOR Register="0" Spot="0" CMYK="#00234111" NAME="PeachPuff2" />
+  <COLOR Register="0" Spot="0" CMYK="#001e3832" NAME="PeachPuff3" />
+  <COLOR Register="0" Spot="0" CMYK="#00142674" NAME="PeachPuff4" />
+  <COLOR Register="0" Spot="0" CMYK="#00488e32" NAME="Peru" />
+  <COLOR Register="0" Spot="0" CMYK="#003f3400" NAME="Pink" />
+  <COLOR Register="0" Spot="0" CMYK="#004a3a00" NAME="Pink1" />
+  <COLOR Register="0" Spot="0" CMYK="#00453611" NAME="Pink2" />
+  <COLOR Register="0" Spot="0" CMYK="#003c2f32" NAME="Pink3" />
+  <COLOR Register="0" Spot="0" CMYK="#00281f74" NAME="Pink4" />
+  <COLOR Register="0" Spot="0" CMYK="#003d0022" NAME="Plum" />
+  <COLOR Register="0" Spot="0" CMYK="#00440000" NAME="Plum1" />
+  <COLOR Register="0" Spot="0" CMYK="#00400011" NAME="Plum2" />
+  <COLOR Register="0" Spot="0" CMYK="#00370032" NAME="Plum3" />
+  <COLOR Register="0" Spot="0" CMYK="#00250074" NAME="Plum4" />
+  <COLOR Register="0" Spot="0" CMYK="#36060019" NAME="PowderBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#50d0000f" NAME="Purple" />
+  <COLOR Register="0" Spot="0" CMYK="#64cf0000" NAME="Purple1" />
+  <COLOR Register="0" Spot="0" CMYK="#5dc20011" NAME="Purple2" />
+  <COLOR Register="0" Spot="0" CMYK="#50a70032" NAME="Purple3" />
+  <COLOR Register="0" Spot="0" CMYK="#36710074" NAME="Purple4" />
+  <COLOR Register="0" Spot="0" CMYK="#00ffff00" NAME="Red" />
+  <COLOR Register="0" Spot="0" CMYK="#00ffff00" NAME="Red1" />
+  <COLOR Register="0" Spot="0" CMYK="#00eeee11" NAME="Red2" />
+  <COLOR Register="0" Spot="0" CMYK="#00cdcd32" NAME="Red3" />
+  <COLOR Register="0" Spot="0" CMYK="#008b8b74" NAME="Red4" />
+  <COLOR Register="0" Spot="0" CMYK="#002d2d43" NAME="RosyBrown" />
+  <COLOR Register="0" Spot="0" CMYK="#003e3e00" NAME="RosyBrown1" />
+  <COLOR Register="0" Spot="0" CMYK="#003a3a11" NAME="RosyBrown2" />
+  <COLOR Register="0" Spot="0" CMYK="#00323232" NAME="RosyBrown3" />
+  <COLOR Register="0" Spot="0" CMYK="#00222274" NAME="RosyBrown4" />
+  <COLOR Register="0" Spot="0" CMYK="#a078001e" NAME="RoyalBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#b7890000" NAME="RoyalBlue1" />
+  <COLOR Register="0" Spot="0" CMYK="#ab800011" NAME="RoyalBlue2" />
+  <COLOR Register="0" Spot="0" CMYK="#936e0032" NAME="RoyalBlue3" />
+  <COLOR Register="0" Spot="0" CMYK="#644b0074" NAME="RoyalBlue4" />
+  <COLOR Register="0" Spot="0" CMYK="#00467874" NAME="SaddleBrown" />
+  <COLOR Register="0" Spot="0" CMYK="#007a8805" NAME="Salmon" />
+  <COLOR Register="0" Spot="0" CMYK="#00739600" NAME="Salmon1" />
+  <COLOR Register="0" Spot="0" CMYK="#006c8c11" NAME="Salmon2" />
+  <COLOR Register="0" Spot="0" CMYK="#005d7932" NAME="Salmon3" />
+  <COLOR Register="0" Spot="0" CMYK="#003f5274" NAME="Salmon4" />
+  <COLOR Register="0" Spot="0" CMYK="#0050940b" NAME="SandyBrown" />
+  <COLOR Register="0" Spot="0" CMYK="#5d003474" NAME="SeaGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#ab006000" NAME="SeaGreen1" />
+  <COLOR Register="0" Spot="0" CMYK="#a0005a11" NAME="SeaGreen2" />
+  <COLOR Register="0" Spot="0" CMYK="#8a004d32" NAME="SeaGreen3" />
+  <COLOR Register="0" Spot="0" CMYK="#5d003474" NAME="SeaGreen4" />
+  <COLOR Register="0" Spot="0" CMYK="#000a1100" NAME="Seashell" />
+  <COLOR Register="0" Spot="0" CMYK="#000a1100" NAME="Seashell1" />
+  <COLOR Register="0" Spot="0" CMYK="#00091011" NAME="Seashell2" />
+  <COLOR Register="0" Spot="0" CMYK="#00080e32" NAME="Seashell3" />
+  <COLOR Register="0" Spot="0" CMYK="#00050974" NAME="Seashell4" />
+  <COLOR Register="0" Spot="0" CMYK="#004e735f" NAME="Sienna" />
+  <COLOR Register="0" Spot="0" CMYK="#007db800" NAME="Sienna1" />
+  <COLOR Register="0" Spot="0" CMYK="#0075ac11" NAME="Sienna2" />
+  <COLOR Register="0" Spot="0" CMYK="#00659432" NAME="Sienna3" />
+  <COLOR Register="0" Spot="0" CMYK="#00446574" NAME="Sienna4" />
+  <COLOR Register="0" Spot="0" CMYK="#641d0014" NAME="SkyBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#78310000" NAME="SkyBlue1" />
+  <COLOR Register="0" Spot="0" CMYK="#702e0011" NAME="SkyBlue2" />
+  <COLOR Register="0" Spot="0" CMYK="#61270032" NAME="SkyBlue3" />
+  <COLOR Register="0" Spot="0" CMYK="#411b0074" NAME="SkyBlue4" />
+  <COLOR Register="0" Spot="0" CMYK="#63730032" NAME="SlateBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#7c900000" NAME="SlateBlue1" />
+  <COLOR Register="0" Spot="0" CMYK="#74870011" NAME="SlateBlue2" />
+  <COLOR Register="0" Spot="0" CMYK="#64740032" NAME="SlateBlue3" />
+  <COLOR Register="0" Spot="0" CMYK="#444f0074" NAME="SlateBlue4" />
+  <COLOR Register="0" Spot="0" CMYK="#2010006f" NAME="SlateGrey" />
+  <COLOR Register="0" Spot="0" CMYK="#391d0000" NAME="SlateGrey1" />
+  <COLOR Register="0" Spot="0" CMYK="#351b0011" NAME="SlateGrey2" />
+  <COLOR Register="0" Spot="0" CMYK="#2e170032" NAME="SlateGrey3" />
+  <COLOR Register="0" Spot="0" CMYK="#1f100074" NAME="SlateGrey4" />
+  <COLOR Register="0" Spot="0" CMYK="#00050500" NAME="Snow" />
+  <COLOR Register="0" Spot="0" CMYK="#00050500" NAME="Snow1" />
+  <COLOR Register="0" Spot="0" CMYK="#00050511" NAME="Snow2" />
+  <COLOR Register="0" Spot="0" CMYK="#00040432" NAME="Snow3" />
+  <COLOR Register="0" Spot="0" CMYK="#00020274" NAME="Snow4" />
+  <COLOR Register="0" Spot="0" CMYK="#ff008000" NAME="SpringGreen" />
+  <COLOR Register="0" Spot="0" CMYK="#ff008000" NAME="SpringGreen1" />
+  <COLOR Register="0" Spot="0" CMYK="#ee007811" NAME="SpringGreen2" />
+  <COLOR Register="0" Spot="0" CMYK="#cd006732" NAME="SpringGreen3" />
+  <COLOR Register="0" Spot="0" CMYK="#8b004674" NAME="SpringGreen4" />
+  <COLOR Register="0" Spot="0" CMYK="#6e32004b" NAME="SteelBlue" />
+  <COLOR Register="0" Spot="0" CMYK="#9c470000" NAME="SteelBlue1" />
+  <COLOR Register="0" Spot="0" CMYK="#92420011" NAME="SteelBlue2" />
+  <COLOR Register="0" Spot="0" CMYK="#7e390032" NAME="SteelBlue3" />
+  <COLOR Register="0" Spot="0" CMYK="#55270074" NAME="SteelBlue4" />
+  <COLOR Register="0" Spot="0" CMYK="#001e462d" NAME="Tan" />
+  <COLOR Register="0" Spot="0" CMYK="#005ab000" NAME="Tan1" />
+  <COLOR Register="0" Spot="0" CMYK="#0054a511" NAME="Tan2" />
+  <COLOR Register="0" Spot="0" CMYK="#00488e32" NAME="Tan3" />
+  <COLOR Register="0" Spot="0" CMYK="#00316074" NAME="Tan4" />
+  <COLOR Register="0" Spot="0" CMYK="#00190027" NAME="Thistle" />
+  <COLOR Register="0" Spot="0" CMYK="#001e0000" NAME="Thistle1" />
+  <COLOR Register="0" Spot="0" CMYK="#001c0011" NAME="Thistle2" />
+  <COLOR Register="0" Spot="0" CMYK="#00180032" NAME="Thistle3" />
+  <COLOR Register="0" Spot="0" CMYK="#00100074" NAME="Thistle4" />
+  <COLOR Register="0" Spot="0" CMYK="#009cb800" NAME="Tomato" />
+  <COLOR Register="0" Spot="0" CMYK="#009cb800" NAME="Tomato1" />
+  <COLOR Register="0" Spot="0" CMYK="#0092ac11" NAME="Tomato2" />
+  <COLOR Register="0" Spot="0" CMYK="#007e9432" NAME="Tomato3" />
+  <COLOR Register="0" Spot="0" CMYK="#00556574" NAME="Tomato4" />
+  <COLOR Register="0" Spot="0" CMYK="#a000101f" NAME="Turquoise" />
+  <COLOR Register="0" Spot="0" CMYK="#ff0a0000" NAME="Turquoise1" />
+  <COLOR Register="0" Spot="0" CMYK="#ee090011" NAME="Turquoise2" />
+  <COLOR Register="0" Spot="0" CMYK="#cd080032" NAME="Turquoise3" />
+  <COLOR Register="0" Spot="0" CMYK="#8b050074" NAME="Turquoise4" />
+  <COLOR Register="0" Spot="0" CMYK="#006c0011" NAME="Violet" />
+  <COLOR Register="0" Spot="0" CMYK="#00b0402f" NAME="VioletRed" />
+  <COLOR Register="0" Spot="0" CMYK="#00c16900" NAME="VioletRed1" />
+  <COLOR Register="0" Spot="0" CMYK="#00b46211" NAME="VioletRed2" />
+  <COLOR Register="0" Spot="0" CMYK="#009b5532" NAME="VioletRed3" />
+  <COLOR Register="0" Spot="0" CMYK="#00693974" NAME="VioletRed4" />
+  <COLOR Register="0" Spot="0" CMYK="#0017420a" NAME="Wheat" />
+  <COLOR Register="0" Spot="0" CMYK="#00184500" NAME="Wheat1" />
+  <COLOR Register="0" Spot="0" CMYK="#00164011" NAME="Wheat2" />
+  <COLOR Register="0" Spot="0" CMYK="#00133732" NAME="Wheat3" />
+  <COLOR Register="0" Spot="0" CMYK="#000d2574" NAME="Wheat4" />
+  <COLOR Register="0" Spot="0" CMYK="#00000000" NAME="White" />
+  <COLOR Register="0" Spot="0" CMYK="#0000000a" NAME="WhiteSmoke" />
+  <COLOR Register="0" Spot="0" CMYK="#0000ff00" NAME="Yellow" />
+  <COLOR Register="0" Spot="0" CMYK="#0000ff00" NAME="Yellow1" />
+  <COLOR Register="0" Spot="0" CMYK="#0000ee11" NAME="Yellow2" />
+  <COLOR Register="0" Spot="0" CMYK="#0000cd32" NAME="Yellow3" />
+  <COLOR Register="0" Spot="0" CMYK="#00008b74" NAME="Yellow4" />
+  <COLOR Register="0" Spot="0" CMYK="#33009b32" NAME="YellowGreen" />
+  <STYLE ALIGN="0" LINESP="15" VOR="0" DROPLIN="2" RMARGIN="0" PSHORTCUT="" LINESPMode="0" NACH="0" SHORTCUT="" CNAME="" DROPDIST="0" FIRST="0" INDENT="0" NAME="Default Paragraph Style" DROP="0" />
+  <CHARSTYLE SCOLOR="Black" TXTSHX="5" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="Default Character Style" FONTSIZE="12" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Arial Regular" TXTSTP="-0.1" TXTULW="-0.1" />
+  <CHARSTYLE SCOLOR="Black" TXTSHX="5" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="Copy of Default Character Style (1)" FONTSIZE="12" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Arial Regular" TXTSTP="-0.1" TXTULW="-0.1" />
+  <CHARSTYLE SCOLOR="Black" TXTSHX="5" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="Copy of Default Character Style (2)" FONTSIZE="12" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Arial Regular" TXTSTP="-0.1" TXTULW="-0.1" />
+  <CHARSTYLE SCOLOR="Black" TXTSHX="5" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="Copy of Default Character Style (3)" FONTSIZE="12" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Arial Regular" TXTSTP="-0.1" TXTULW="-0.1" />
+  <LAYERS DRUCKEN="1" NUMMER="0" TRANS="1" LAYERC="#000000" EDIT="1" NAME="Hintergrund" FLOW="1" OUTL="0" BLEND="0" SICHTBAR="1" LEVEL="0" />
+  <Printer mirrorH="0" BleedBottom="0" colorMarks="1" cropMarks="1" doOverprint="1" useColor="0" firstUse="1" useICC="0" printerCommand="" BleedRight="0" useSpotColors="0" outputSeparations="0" toFile="0" doGCR="0" registrationMarks="1" doClip="0" markOffset="1.04394e-265" useAltPrintCommand="0" BleedTop="0" printer="" BleedLeft="0" bleedMarks="1" setDevParam="0" separationName="" PSLevel="72" filename="" mirrorV="0" />
+  <PDF displayThumbs="0" ImagePr="0" fitWindow="0" displayBookmarks="0" doOverprint="0" colorMarks="0" cropMarks="0" BTop="9" UseProfiles="0" BLeft="9" PrintP="" RecalcPic="0" firstUse="1" UseSpotColors="1" ImageP="" SolidP="" PicRes="300" Thumbnails="0" hideToolBar="0" registrationMarks="0" CMethod="0" displayLayers="0" doMultiFile="0" UseLayers="0" Encrypt="0" markOffset="0" BRight="9" Binding="0" Articles="0" useDocBleeds="0" InfoString="" RGBMode="1" Grayscale="0" PresentMode="0" openAction="" displayFullscreen="0" Permissions="-4" bleedMarks="0" Intent="1" Compress="1" hideMenuBar="0" Version="14" Resolution="300" Bookmarks="0" docInfoMarks="0" UseProfiles2="0" RotateDeg="0" Clip="0" MirrorV="0" Quality="0" PageLayout="0" UseLpi="0" PassUser="" BBottom="9" Intent2="1" MirrorH="0" PassOwner="" >
+   <LPI Angle="45" Frequency="75" SpotFunction="2" Color="Black" />
+   <LPI Angle="105" Frequency="75" SpotFunction="2" Color="Cyan" />
+   <LPI Angle="75" Frequency="75" SpotFunction="2" Color="Magenta" />
+   <LPI Angle="90" Frequency="75" SpotFunction="2" Color="Yellow" />
+  </PDF>
+  <DocItemAttributes/>
+  <TablesOfContents/>
+  <PageSets>
+   <Set Columns="1" GapBelow="40" Rows="1" FirstPage="0" GapHorizontal="0" Name="Single Page" GapVertical="0" />
+   <Set Columns="2" GapBelow="40" Rows="1" FirstPage="1" GapHorizontal="0" Name="Double Sided" GapVertical="0" >
+    <PageNames Name="Left Page" />
+    <PageNames Name="Right Page" />
+   </Set>
+   <Set Columns="3" GapBelow="40" Rows="1" FirstPage="0" GapHorizontal="0" Name="3-Fold" GapVertical="0" >
+    <PageNames Name="Left Page" />
+    <PageNames Name="Middle" />
+    <PageNames Name="Right Page" />
+   </Set>
+   <Set Columns="4" GapBelow="40" Rows="1" FirstPage="0" GapHorizontal="0" Name="4-Fold" GapVertical="0" >
+    <PageNames Name="Left Page" />
+    <PageNames Name="Middle Left" />
+    <PageNames Name="Middle Right" />
+    <PageNames Name="Right Page" />
+   </Set>
+  </PageSets>
+  <Sections>
+   <Section Active="1" Number="0" From="0" Type="Type_1_2_3" To="0" Name="0" Start="1" Reversed="0" />
+  </Sections>
+  <MASTERPAGE AGhorizontalAutoCount="0" AGverticalAutoCount="0" AGhorizontalAutoGap="0" Size="A6" NUM="0" BORDERTOP="9" NAM="Normal" LEFT="0" BORDERBOTTOM="9" AGSelection="5.29981e-315 0 0 5.29981e-315" Orientation="1" AGverticalAutoGap="0" BORDERRIGHT="9" PAGEHEIGHT="297.64" PAGEWIDTH="419.53" PAGEYPOS="20" AGverticalAutoRefer="0" HorizontalGuides="" MNAM="" PAGEXPOS="100" AGhorizontalAutoRefer="0" VerticalGuides="" BORDERLEFT="9" />
+  <PAGE AGhorizontalAutoCount="0" AGverticalAutoCount="0" AGhorizontalAutoGap="0" Size="A5" NUM="0" BORDERTOP="9" NAM="" LEFT="0" BORDERBOTTOM="9" AGSelection="5.29981e-315 0 0 5.29981e-315" Orientation="1" AGverticalAutoGap="0" BORDERRIGHT="9" PAGEHEIGHT="419.53" PAGEWIDTH="595.28" PAGEYPOS="20" AGverticalAutoRefer="0" HorizontalGuides="" MNAM="Normal" PAGEXPOS="100" AGhorizontalAutoRefer="0" VerticalGuides="" BORDERLEFT="9" />
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="-1.79417e+308" gWidth="-1.79417e+308" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="196" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Beige" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="0" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="372" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 196 0 196 0 196 0 196 0 196 372 196 372 196 372 196 372 0 372 0 372 0 372 0 372 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="-1.79417e+308" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="-1.79417e+308" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="43" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="483" NUMCO="16" POCOOR="0 0 0 0 196 0 196 0 196 0 196 0 196 372 196 372 196 372 196 372 0 372 0 372 0 372 0 372 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="3.63841e-304" gWidth="3.63841e-304" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="116" ImageRes="1" GROUPS="59 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Bisque3" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1.9963" HEIGHT="122" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="1" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 116 0 116 0 116 0 116 0 116 122 116 122 116 122 116 122 0 122 0 122 0 122 0 122 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="3.63841e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="3.63841e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="228" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="519" NUMCO="16" POCOOR="0 0 0 0 116 0 116 0 116 0 116 0 116 122 116 122 116 122 116 122 0 122 0 122 0 122 0 122 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="1" gHeight="7.29112e-304" gWidth="7.29112e-304" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="76.85" ImageRes="1" GROUPS="59 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1.9963" HEIGHT="25.0741" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 76.85 0 76.85 0 76.85 0 76.85 0 76.85 25.0741 76.85 25.0741 76.85 25.0741 76.85 25.0741 0 25.0741 0 25.0741 0 25.0741 0 25.0741 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="7.29112e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="7.29112e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="234.926" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="538.65" NUMCO="16" POCOOR="0 0 0 0 76.85 0 76.85 0 76.85 0 76.85 0 76.85 25.0741 76.85 25.0741 76.85 25.0741 76.85 25.0741 0 25.0741 0 25.0741 0 25.0741 0 25.0741 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT SCOLOR="Black" TXTSHX="5" TXTSHY="-5" CH="Storage" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <para SCOLOR="Black" ALIGN="1" TXTSHX="5" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" PSHORTCUT="" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" NAME="" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="5.00662e-308" gWidth="1.60219e-306" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="144" ImageRes="1" GROUPS="3 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Aquamarine3" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="1" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="42" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="144 21 144 32.5984 72 42 111.766 42 72 42 32.2355 42 0 21 0 32.5984 0 21 0 9.40202 72 0 32.2355 0 72 0 111.766 0 144 21 144 9.40202 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="1.11261e-306" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="4.4505e-308" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="253" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="136" NUMCO="16" POCOOR="144 21 144 32.5984 72 42 111.766 42 72 42 32.2355 42 0 21 0 32.5984 0 21 0 9.40202 72 0 32.2355 0 72 0 111.766 0 144 21 144 9.40202 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="5.33666e-315" gWidth="5.32028e-315" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="100" ImageRes="1" GROUPS="3 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="17" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 100 0 100 0 100 0 100 0 100 17 100 17 100 17 100 17 0 17 0 17 0 17 0 17 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="5.33663e-315" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="5.31963e-315" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="266" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="158" NUMCO="16" POCOOR="0 0 0 0 100 0 100 0 100 0 100 0 100 17 100 17 100 17 100 17 0 17 0 17 0 17 0 17 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT SCOLOR="Black" TXTSHX="5" TXTSHY="-5" CH="Agent (Resource)" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <para SCOLOR="Black" ALIGN="0" TXTSHX="5" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" PSHORTCUT="" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" NAME="" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="-9.88736e-117" gWidth="1.86808e-312" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="107" ImageRes="1" GROUPS="4 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="CadetBlue3" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="47" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 107 0 107 0 107 0 107 0 107 47 107 47 107 47 107 47 0 47 0 47 0 47 0 47 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="2.122e-314" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="9.34202e+20" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="47" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="524" NUMCO="16" POCOOR="0 0 0 0 107 0 107 0 107 0 107 0 107 47 107 47 107 47 107 47 0 47 0 47 0 47 0 47 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="1" gHeight="-1.79417e+308" gWidth="-1.79417e+308" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="68" ImageRes="1" GROUPS="4 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="20" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 68 0 68 0 68 0 68 0 68 20 68 20 68 20 68 20 0 20 0 20 0 20 0 20 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="-1.79417e+308" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="-1.79417e+308" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="61" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="544" NUMCO="16" POCOOR="0 0 0 0 68 0 68 0 68 0 68 0 68 20 68 20 68 20 68 20 0 20 0 20 0 20 0 20 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT SCOLOR="Black" TXTSHX="5" TXTSHY="-5" CH="Control" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <para SCOLOR="Black" ALIGN="1" TXTSHX="5" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" PSHORTCUT="" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" NAME="" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="7.29112e-304" gWidth="7.29112e-304" NUMPO="0" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="145.751" TXTSHX="5" TXTSTROKE="Black" WIDTH="113.719" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="-43.5" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="0" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="5" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="1" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="" BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="7.29112e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="7.29112e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="200" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="358" NUMCO="0" POCOOR="" EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="8.47917e-305" gWidth="8.47917e-305" NUMPO="0" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="170.059" TXTSHX="5" TXTSTROKE="Black" WIDTH="98.4784" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="0" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="5" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="1" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="" BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="8.47917e-305" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="8.47917e-305" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="188" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="362" NUMCO="0" POCOOR="" EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="-1.79417e+308" gWidth="-1.79417e+308" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="144" ImageRes="1" GROUPS="1 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Aquamarine3" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="1" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="42" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="144 21 144 32.5984 72 42 111.766 42 72 42 32.2355 42 0 21 0 32.5984 0 21 0 9.40202 72 0 32.2355 0 72 0 111.766 0 144 21 144 9.40202 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="-1.79417e+308" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="-1.79417e+308" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="185" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="122" NUMCO="16" POCOOR="144 21 144 32.5984 72 42 111.766 42 72 42 32.2355 42 0 21 0 32.5984 0 21 0 9.40202 72 0 32.2355 0 72 0 111.766 0 144 21 144 9.40202 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="7.29112e-304" gWidth="7.29112e-304" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="100" ImageRes="1" GROUPS="1 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="17" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 100 0 100 0 100 0 100 0 100 17 100 17 100 17 100 17 0 17 0 17 0 17 0 17 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="7.29112e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="7.29112e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="198" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="144" NUMCO="16" POCOOR="0 0 0 0 100 0 100 0 100 0 100 0 100 17 100 17 100 17 100 17 0 17 0 17 0 17 0 17 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT SCOLOR="Black" TXTSHX="5" TXTSHY="-5" CH="Agent (Resource)" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <para SCOLOR="Black" ALIGN="0" TXTSHX="5" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" PSHORTCUT="" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" NAME="" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="1" BottomLine="0" REXTRA="0" gHeight="1.80108e-226" gWidth="1.80108e-226" NUMPO="16" groupsLastItem="2" TransBlendS="0" PLINEART="1" doOverprint="0" RightLine="0" LOCALSCX="1" ROT="0" WIDTH="94" ImageRes="1" GROUPS="64 " LOCKR="0" LOCALSCY="1" NAMEDLST="" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Black" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" FRTYPE="0" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" PWIDTH="0" HEIGHT="27" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" textPathType="0" PLTSHOW="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="Group64" SHADE="100" fillRule="1" COCOOR="0 0 0 0 94 0 94 0 94 0 94 0 94 27 94 27 94 27 94 27 0 27 0 27 0 27 0 27 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="1.80108e-226" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="1.80108e-226" DASHS="" IRENDER="1" TEXTFLOW="0" YPOS="93.5" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="530.5" NUMCO="16" POCOOR="0 0 0 0 94 0 94 0 94 0 94 0 94 27 94 27 94 27 94 27 0 27 0 27 0 27 0 27 0 0 0 0 " EXTRA="0" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="0" SCALETYPE="1" endArrowIndex="0" TransBlend="0" BEXTRA="0" PLINEJOIN="0" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="26" gWidth="93" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="93" ImageRes="1" GROUPS="64 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="CadetBlue2" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="26" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 93 0 93 0 93 0 93 0 93 26 93 26 93 26 93 26 0 26 0 26 0 26 0 26 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="0" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="0" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="94" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="531" NUMCO="16" POCOOR="0 0 0 0 93 0 93 0 93 0 93 0 93 26 93 26 93 26 93 26 0 26 0 26 0 26 0 26 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" gHeight="26" gWidth="93" NUMPO="16" TransBlendS="0" PLINEART="1" doOverprint="0" RightLine="0" LOCALSCX="1" ROT="0" WIDTH="86" ImageRes="1" GROUPS="64 " LOCKR="0" LOCALSCY="1" NAMEDLST="" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" PWIDTH="1" HEIGHT="19" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" textPathType="0" PLTSHOW="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" fillRule="1" COCOOR="0 0 0 0 86 0 86 0 86 0 86 0 86 19 86 19 86 19 86 19 0 19 0 19 0 19 0 19 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="4" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="5" DASHS="" IRENDER="1" TEXTFLOW="0" YPOS="98" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="536" NUMCO="16" POCOOR="0 0 0 0 86 0 86 0 86 0 86 0 86 19 86 19 86 19 86 19 0 19 0 19 0 19 0 19 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" >
+   <ITEXT SCOLOR="Black" TXTSHX="5" TXTSHY="-5" CH="D-Bus interface" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <para SCOLOR="Black" ALIGN="1" TXTSHX="5" LINESP="14.4" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" PSHORTCUT="" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" NAME="" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="1" gHeight="5.00362e-304" gWidth="5.00362e-304" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="134" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="0" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="18" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 134 0 134 0 134 0 134 0 134 18 134 18 134 18 134 18 0 18 0 18 0 18 0 18 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="5.00362e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="5.00362e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="393" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="511" NUMCO="16" POCOOR="0 0 0 0 134 0 134 0 134 0 134 0 134 18 134 18 134 18 134 18 0 18 0 18 0 18 0 18 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT SCOLOR="Black" TXTSHX="5" TXTSHY="-5" CH="Akonadi Server" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <para SCOLOR="Black" ALIGN="1" TXTSHX="5" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" PSHORTCUT="" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" NAME="" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="2.122e-314" gWidth="0" NUMPO="0" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="-0.516164" TXTSHX="5" TXTSTROKE="Black" WIDTH="111.005" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="0" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="5" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="1" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="" BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="2.122e-314" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="0" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="134" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="279" NUMCO="0" POCOOR="" EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="7.29112e-304" gWidth="1.78019e-306" NUMPO="0" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="18.9465" TXTSHX="5" TXTSTROKE="Black" WIDTH="141.676" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="0" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="5" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="1" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="" BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="1.60219e-306" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="7.56604e-307" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="75" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="254" NUMCO="0" POCOOR="" EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="7.29112e-304" gWidth="7.29112e-304" NUMPO="0" TransBlendS="0" PLINEART="2" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="-11.8887" TXTSHX="5" TXTSTROKE="Black" WIDTH="116.499" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="0" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="5" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="1" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="" BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="7.29112e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="7.29112e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="145" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="433" NUMCO="0" POCOOR="" EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="279" gWidth="46" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="46" ImageRes="1" GROUPS="24 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="DarkSeaGreen3" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1.25362" HEIGHT="279" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="1" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 46 0 46 0 46 0 46 0 46 279 46 279 46 279 46 279 0 279 0 279 0 279 0 279 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="0" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="0" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="51" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="388" NUMCO="16" POCOOR="0 0 0 0 46 0 46 0 46 0 46 0 46 279 46 279 46 279 46 279 0 279 0 279 0 279 0 279 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="279" gWidth="46" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="-90" TXTSHX="5" TXTSTROKE="Black" WIDTH="106.078" ImageRes="1" GROUPS="24 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1.25362" HEIGHT="19" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="1" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 3.26153e-14 -70.2098 3.26153e-14 -70.2098 3.26153e-14 -70.2098 3.26153e-14 -70.2098 28.7066 -70.2098 28.7066 -70.2098 28.7066 -70.2098 28.7066 -70.2098 28.7066 6.05424e-15 28.7066 6.05424e-15 28.7066 6.05424e-15 28.7066 6.05424e-15 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="190.359" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="13" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="241.359" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="401" NUMCO="16" POCOOR="0 1.16613e-15 0 1.16613e-15 106.078 0 106.078 0 106.078 0 106.078 0 106.078 19 106.078 19 106.078 19 106.078 19 0 19 0 19 0 19 0 19 0 1.16613e-15 0 1.16613e-15 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT SCOLOR="Black" TXTSHX="5" TXTSHY="-5" CH="libakonadi" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <para SCOLOR="Black" ALIGN="0" TXTSHX="5" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" PSHORTCUT="" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" NAME="" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="7.29112e-304" gWidth="7.29112e-304" NUMPO="0" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="19.5367" TXTSHX="5" TXTSTROKE="Black" WIDTH="98.6813" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="0" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="5" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="1" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="" BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="7.29112e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="7.29112e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="183" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="433" NUMCO="0" POCOOR="" EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="1" BottomLine="0" REXTRA="0" gHeight="30.0991" gWidth="112.55" NUMPO="16" groupsLastItem="2" TransBlendS="0" PLINEART="1" doOverprint="0" RightLine="0" LOCALSCX="1" ROT="0" WIDTH="112.55" ImageRes="1" GROUPS="61 " LOCKR="0" LOCALSCY="1" NAMEDLST="" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Black" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" FRTYPE="0" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" PWIDTH="0.01" HEIGHT="30.0991" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" textPathType="0" PLTSHOW="0" CLIPEDIT="1" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="Group61" SHADE="100" fillRule="1" COCOOR="0 0 0 0 112.55 0 112.55 0 112.55 0 112.55 0 112.55 30.0991 112.55 30.0991 112.55 30.0991 112.55 30.0991 0 30.0991 0 30.0991 0 30.0991 0 30.0991 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="0" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="0" DASHS="" IRENDER="1" TEXTFLOW="0" YPOS="198.45" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="520.45" NUMCO="16" POCOOR="0 0 0 0 112.55 0 112.55 0 112.55 0 112.55 0 112.55 30.0991 112.55 30.0991 112.55 30.0991 112.55 30.0991 0 30.0991 0 30.0991 0 30.0991 0 30.0991 0 0 0 0 " EXTRA="0" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="0" SCALETYPE="1" endArrowIndex="0" TransBlend="0" BEXTRA="0" PLINEJOIN="0" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="30.0991" gWidth="112.55" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="111.426" ImageRes="1" GROUPS="61 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Bisque2" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1.11137" HEIGHT="29" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="1" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 111.426 0 111.426 0 111.426 0 111.426 0 111.426 29 111.426 29 111.426 29 111.426 29 0 29 0 29 0 29 0 29 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="0.54957" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="0.561802" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="199" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="521.012" NUMCO="16" POCOOR="0 0 0 0 111.426 0 111.426 0 111.426 0 111.426 0 111.426 29 111.426 29 111.426 29 111.426 29 0 29 0 29 0 29 0 29 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" gHeight="30.0991" gWidth="112.55" NUMPO="16" TransBlendS="0" PLINEART="1" doOverprint="0" RightLine="0" LOCALSCX="1" ROT="0" WIDTH="102.6" ImageRes="1" GROUPS="61 " LOCKR="0" LOCALSCY="1" NAMEDLST="" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" PWIDTH="1.11137" HEIGHT="16.1111" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" textPathType="0" PLTSHOW="0" CLIPEDIT="1" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" fillRule="1" COCOOR="0 0 0 0 102.6 0 102.6 0 102.6 0 102.6 0 102.6 16.1111 102.6 16.1111 102.6 16.1111 102.6 16.1111 0 16.1111 0 16.1111 0 16.1111 0 16.1111 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="6.99457" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="6.0779" DASHS="" IRENDER="1" TEXTFLOW="0" YPOS="205.445" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="526.528" NUMCO="16" POCOOR="0 0 0 0 102.6 0 102.6 0 102.6 0 102.6 0 102.6 16.1111 102.6 16.1111 102.6 16.1111 102.6 16.1111 0 16.1111 0 16.1111 0 16.1111 0 16.1111 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" >
+   <ITEXT SCOLOR="Black" TXTSHX="5" TXTSHY="-5" CH="IMAP interface" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <para SCOLOR="Black" ALIGN="1" TXTSHX="5" LINESP="14.4" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" PSHORTCUT="" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" NAME="" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="1.61328e-307" gWidth="1.6912e-306" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="31" ImageRes="1" GROUPS="53 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Aquamarine4" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="72.7448" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 31 0 31 0 31 0 31 0 31 72.7448 31 72.7448 31 72.7448 31 72.7448 0 72.7448 0 72.7448 0 72.7448 0 72.7448 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="4.00524e-307" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="3.11524e-307" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="244" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="356" NUMCO="16" POCOOR="0 0 0 0 31 0 31 0 31 0 31 0 31 72.7448 31 72.7448 31 72.7448 31 72.7448 0 72.7448 0 72.7448 0 72.7448 0 72.7448 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="1" gHeight="5.28469e-308" gWidth="5.56295e-307" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="-90" TXTSHX="5" TXTSTROKE="Black" WIDTH="48" ImageRes="1" GROUPS="53 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="18" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 48 0 48 0 48 0 48 0 48 18 48 18 48 18 48 18 0 18 0 18 0 18 0 18 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="5.00662e-308" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="1.78021e-306" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="303" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="362" NUMCO="16" POCOOR="0 0 0 0 48 0 48 0 48 0 48 0 48 18 48 18 48 18 48 18 0 18 0 18 0 18 0 18 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT SCOLOR="Black" TXTSHX="5" TXTSHY="-5" CH="libkabc" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <para SCOLOR="Black" ALIGN="1" TXTSHX="5" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" PSHORTCUT="" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" NAME="" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="-1.79417e+308" gWidth="-1.79417e+308" NUMPO="48" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="92" ImageRes="1" GROUPS="55 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Azure" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="5" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="55" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="1" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 13.75 0 13.75 23 13.75 23 13.75 23 13.75 23 13.75 23 0 23 0 23 0 23 0 69 0 69 0 69 0 69 0 69 13.75 69 13.75 69 13.75 69 13.75 92 13.75 92 13.75 92 13.75 92 13.75 92 41.25 92 41.25 92 41.25 92 41.25 69 41.25 69 41.25 69 41.25 69 41.25 69 55 69 55 69 55 69 55 23 55 23 55 23 55 23 55 23 41.25 23 41.25 23 41.25 23 41.25 0 41.25 0 41.25 0 41.25 0 41.25 0 13.75 0 13.75 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="-1.79417e+308" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="-1.79417e+308" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="319" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="224" NUMCO="48" POCOOR="0 13.75 0 13.75 23 13.75 23 13.75 23 13.75 23 13.75 23 0 23 0 23 0 23 0 69 0 69 0 69 0 69 0 69 13.75 69 13.75 69 13.75 69 13.75 92 13.75 92 13.75 92 13.75 92 13.75 92 41.25 92 41.25 92 41.25 92 41.25 69 41.25 69 41.25 69 41.25 69 41.25 69 55 69 55 69 55 69 55 23 55 23 55 23 55 23 55 23 41.25 23 41.25 23 41.25 23 41.25 0 41.25 0 41.25 0 41.25 0 41.25 0 13.75 0 13.75 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="1" gHeight="7.29112e-304" gWidth="7.29112e-304" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="72" ImageRes="1" GROUPS="55 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="23" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 72 0 72 0 72 0 72 0 72 23 72 23 72 23 72 23 0 23 0 23 0 23 0 23 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="7.29112e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="7.29112e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="339" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="232" NUMCO="16" POCOOR="0 0 0 0 72 0 72 0 72 0 72 0 72 23 72 23 72 23 72 23 0 23 0 23 0 23 0 23 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT SCOLOR="Black" TXTSHX="5" TXTSHY="-5" CH="Application" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <para SCOLOR="Black" ALIGN="1" TXTSHX="5" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" PSHORTCUT="" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" NAME="" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="1.60219e-306" gWidth="7.56604e-307" NUMPO="48" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="92" ImageRes="1" GROUPS="56 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Azure4" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="5" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="55" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="1" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 13.75 0 13.75 23 13.75 23 13.75 23 13.75 23 13.75 23 0 23 0 23 0 23 0 69 0 69 0 69 0 69 0 69 13.75 69 13.75 69 13.75 69 13.75 92 13.75 92 13.75 92 13.75 92 13.75 92 41.25 92 41.25 92 41.25 92 41.25 69 41.25 69 41.25 69 41.25 69 41.25 69 55 69 55 69 55 69 55 23 55 23 55 23 55 23 55 23 41.25 23 41.25 23 41.25 23 41.25 0 41.25 0 41.25 0 41.25 0 41.25 0 13.75 0 13.75 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="9.25535e-266" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="5.73167e-266" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="370" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="301" NUMCO="48" POCOOR="0 13.75 0 13.75 23 13.75 23 13.75 23 13.75 23 13.75 23 0 23 0 23 0 23 0 69 0 69 0 69 0 69 0 69 13.75 69 13.75 69 13.75 69 13.75 92 13.75 92 13.75 92 13.75 92 13.75 92 41.25 92 41.25 92 41.25 92 41.25 69 41.25 69 41.25 69 41.25 69 41.25 69 55 69 55 69 55 69 55 23 55 23 55 23 55 23 55 23 41.25 23 41.25 23 41.25 23 41.25 0 41.25 0 41.25 0 41.25 0 41.25 0 13.75 0 13.75 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="1" gHeight="5.79445e-266" gWidth="0" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="72" ImageRes="1" GROUPS="56 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="23" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 72 0 72 0 72 0 72 0 72 23 72 23 72 23 72 23 0 23 0 23 0 23 0 23 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="5.79525e-266" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="1.5279e-311" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="390" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="309" NUMCO="16" POCOOR="0 0 0 0 72 0 72 0 72 0 72 0 72 23 72 23 72 23 72 23 0 23 0 23 0 23 0 23 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT SCOLOR="Black" TXTSHX="5" TXTSHY="-5" CH="Component" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <para SCOLOR="Black" ALIGN="1" TXTSHX="5" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" PSHORTCUT="" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" NAME="" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="2.03712e-312" gWidth="7.39983e-316" NUMPO="0" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="-48.2961" TXTSHX="5" TXTSTROKE="Black" WIDTH="135.281" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="-78.5" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="0" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="5" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="1" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="" BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="1.13933e-265" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="8.48798e-314" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="319" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="270" NUMCO="0" POCOOR="" EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="2.03712e-312" gWidth="4.5905e-268" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="31" ImageRes="1" GROUPS="52 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Aquamarine4" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="72.7448" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 31 0 31 0 31 0 31 0 31 72.7448 31 72.7448 31 72.7448 31 72.7448 0 72.7448 0 72.7448 0 72.7448 0 72.7448 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="7.31898e-267" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="0" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="171" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="356" NUMCO="16" POCOOR="0 0 0 0 31 0 31 0 31 0 31 0 31 72.7448 31 72.7448 31 72.7448 31 72.7448 0 72.7448 0 72.7448 0 72.7448 0 72.7448 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="1" gHeight="7.29112e-304" gWidth="7.29112e-304" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="-90" TXTSHX="5" TXTSTROKE="Black" WIDTH="48" ImageRes="1" GROUPS="52 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="18" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 48 0 48 0 48 0 48 0 48 18 48 18 48 18 48 18 0 18 0 18 0 18 0 18 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="7.29112e-304" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="7.29112e-304" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="230" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="362" NUMCO="16" POCOOR="0 0 0 0 48 0 48 0 48 0 48 0 48 18 48 18 48 18 48 18 0 18 0 18 0 18 0 18 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT SCOLOR="Black" TXTSHX="5" TXTSHY="-5" CH="libkcal" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <para SCOLOR="Black" ALIGN="1" TXTSHX="5" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" PSHORTCUT="" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" NAME="" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="2.05323e-312" gWidth="1.51974e-100" NUMPO="0" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="-77.8285" TXTSHX="5" TXTSTROKE="Black" WIDTH="52.1728" ImageRes="1" GROUPS="" LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="0" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="5" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="1" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="" BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="-6.61593e+41" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="-1.5154e+260" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="368" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="349" NUMCO="0" POCOOR="" EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="1.0936e-305" gWidth="1.0936e-305" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="10.8" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="91" ImageRes="1" GROUPS="59 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1" HEIGHT="69" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="9" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 91 0 91 0 91 0 91 0 91 69 91 69 91 69 91 69 0 69 0 69 0 69 0 69 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="1.0936e-305" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="1.0936e-305" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="272" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="533" NUMCO="16" POCOOR="0 0 0 0 91 0 91 0 91 0 91 0 91 69 91 69 91 69 91 69 0 69 0 69 0 69 0 69 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT SCOLOR="Black" TXTSHX="5" TXTSHY="-5" CH="/" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="9" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <para SCOLOR="Black" ALIGN="0" TXTSHX="5" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" PSHORTCUT="" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="9" NAME="" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <ITEXT SCOLOR="Black" TXTSHX="5" TXTSHY="-5" CH="/resource1/contacts" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="9" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <para SCOLOR="Black" ALIGN="0" TXTSHX="5" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" PSHORTCUT="" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="9" NAME="" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <ITEXT SCOLOR="Black" TXTSHX="5" TXTSHY="-5" CH="/resource1/events" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="9" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <para SCOLOR="Black" ALIGN="0" TXTSHX="5" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" PSHORTCUT="" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="9" NAME="" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <ITEXT SCOLOR="Black" TXTSHX="5" TXTSHY="-5" CH="/resource2" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="9" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <para SCOLOR="Black" ALIGN="0" TXTSHX="5" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" PSHORTCUT="" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="9" NAME="" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <ITEXT SCOLOR="Black" TXTSHX="5" TXTSHY="-5" CH="/search" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="9" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <para SCOLOR="Black" ALIGN="0" TXTSHX="5" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" PSHORTCUT="" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="9" NAME="" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="1" BottomLine="0" REXTRA="0" gHeight="1.59927e-308" gWidth="1.73834e-308" NUMPO="16" groupsLastItem="2" TransBlendS="0" PLINEART="1" doOverprint="0" RightLine="0" LOCALSCX="1" ROT="0" WIDTH="155.962" ImageRes="1" GROUPS="63 " LOCKR="0" LOCALSCY="1" NAMEDLST="" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Black" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" FRTYPE="0" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" PWIDTH="0" HEIGHT="43.0379" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" textPathType="0" PLTSHOW="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="Group63" SHADE="100" fillRule="1" COCOOR="0 0 0 0 155.962 0 155.962 0 155.962 0 155.962 0 155.962 43.0379 155.962 43.0379 155.962 43.0379 155.962 43.0379 0 43.0379 0 43.0379 0 43.0379 0 43.0379 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="1.73834e-308" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="1.59927e-308" DASHS="" IRENDER="1" TEXTFLOW="0" YPOS="43.481" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="137.481" NUMCO="16" POCOOR="0 0 0 0 155.962 0 155.962 0 155.962 0 155.962 0 155.962 43.0379 155.962 43.0379 155.962 43.0379 155.962 43.0379 0 43.0379 0 43.0379 0 43.0379 0 43.0379 0 0 0 0 " EXTRA="0" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="0" SCALETYPE="1" endArrowIndex="0" TransBlend="0" BEXTRA="0" PLINEJOIN="0" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" gHeight="42" gWidth="154.924" NUMPO="16" TransBlendS="0" PLINEART="1" doOverprint="0" RightLine="0" LOCALSCX="1" ROT="0" WIDTH="154.924" ImageRes="1" GROUPS="63 " LOCKR="0" LOCALSCY="1" NAMEDLST="" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Aquamarine3" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" FRTYPE="1" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" PWIDTH="1.03793" HEIGHT="42" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" textPathType="0" PLTSHOW="0" CLIPEDIT="1" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" fillRule="1" COCOOR="154.924 21 154.924 32.5984 77.4621 42 120.245 42 77.4621 42 34.681 42 0 21 0 32.5984 0 21 0 9.40202 77.4621 0 34.681 0 77.4621 0 120.245 0 154.924 21 154.924 9.40202 " BASEOF="0" PICART="1" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="0" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="0" DASHS="" IRENDER="1" TEXTFLOW="0" YPOS="44" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="138" NUMCO="16" POCOOR="154.924 21 154.924 32.5984 77.4621 42 120.245 42 77.4621 42 34.681 42 0 21 0 32.5984 0 21 0 9.40202 77.4621 0 34.681 0 77.4621 0 120.245 0 154.924 21 154.924 9.40202 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" gHeight="42" gWidth="154.924" NUMPO="16" TransBlendS="0" PLINEART="1" doOverprint="0" RightLine="0" LOCALSCX="1" ROT="0" WIDTH="109.831" ImageRes="1" GROUPS="63 " LOCKR="0" LOCALSCY="1" NAMEDLST="" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" PWIDTH="1.03793" HEIGHT="17" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" textPathType="0" PLTSHOW="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" fillRule="1" COCOOR="0 0 0 0 109.831 0 109.831 0 109.831 0 109.831 0 109.831 17 109.831 17 109.831 17 109.831 17 0 17 0 17 0 17 0 17 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="13" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="23.669" DASHS="" IRENDER="1" TEXTFLOW="0" YPOS="57" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="161.669" NUMCO="16" POCOOR="0 0 0 0 109.831 0 109.831 0 109.831 0 109.831 0 109.831 17 109.831 17 109.831 17 109.831 17 0 17 0 17 0 17 0 17 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" >
+   <ITEXT SCOLOR="Black" TXTSHX="5" TXTSHY="-5" CH="Agent (Strigi feeder)" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <para SCOLOR="Black" ALIGN="0" TXTSHX="5" LINESP="14.4" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" PSHORTCUT="" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" NAME="" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="1" BottomLine="0" REXTRA="0" gHeight="7.28692e-298" gWidth="7.52584e-298" NUMPO="16" groupsLastItem="2" TransBlendS="0" PLINEART="1" doOverprint="0" RightLine="0" LOCALSCX="1" ROT="0" WIDTH="155.962" ImageRes="1" GROUPS="62 " LOCKR="0" LOCALSCY="1" NAMEDLST="" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Black" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" FRTYPE="0" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" PWIDTH="0" HEIGHT="43.0379" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" textPathType="0" PLTSHOW="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="Group62" SHADE="100" fillRule="1" COCOOR="0 0 0 0 155.962 0 155.962 0 155.962 0 155.962 0 155.962 43.0379 155.962 43.0379 155.962 43.0379 155.962 43.0379 0 43.0379 0 43.0379 0 43.0379 0 43.0379 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="7.52584e-298" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="7.28692e-298" DASHS="" IRENDER="1" TEXTFLOW="0" YPOS="113.481" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="124.557" NUMCO="16" POCOOR="0 0 0 0 155.962 0 155.962 0 155.962 0 155.962 0 155.962 43.0379 155.962 43.0379 155.962 43.0379 155.962 43.0379 0 43.0379 0 43.0379 0 43.0379 0 43.0379 0 0 0 0 " EXTRA="0" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="0" SCALETYPE="1" endArrowIndex="0" TransBlend="0" BEXTRA="0" PLINEJOIN="0" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" gHeight="42" gWidth="154.924" NUMPO="16" TransBlendS="0" PLINEART="1" doOverprint="0" RightLine="0" LOCALSCX="1" ROT="0" WIDTH="154.924" ImageRes="1" GROUPS="62 " LOCKR="0" LOCALSCY="1" NAMEDLST="" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="Aquamarine3" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="Black" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" FRTYPE="1" PTYPE="6" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" PWIDTH="1.03793" HEIGHT="42" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" textPathType="0" PLTSHOW="0" CLIPEDIT="1" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" fillRule="1" COCOOR="154.924 21 154.924 32.5984 77.4621 42 120.245 42 77.4621 42 34.681 42 0 21 0 32.5984 0 21 0 9.40202 77.4621 0 34.681 0 77.4621 0 120.245 0 154.924 21 154.924 9.40202 " BASEOF="0" PICART="1" COLUMNS="1" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="0" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="0" DASHS="" IRENDER="1" TEXTFLOW="0" YPOS="114" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="125.076" NUMCO="16" POCOOR="154.924 21 154.924 32.5984 77.4621 42 120.245 42 77.4621 42 34.681 42 0 21 0 32.5984 0 21 0 9.40202 77.4621 0 34.681 0 77.4621 0 120.245 0 154.924 21 154.924 9.40202 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" >
+   <para PSHORTCUT="" SHORTCUT="" CNAME="" NAME="" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+  <PAGEOBJECT OnMasterPage="" isGroupControl="0" BottomLine="0" REXTRA="1" ALIGN="0" gHeight="42" gWidth="154.924" NUMPO="16" TransBlendS="0" PLINEART="1" TXTSCALE="100" doOverprint="0" RightLine="0" LOCALSCX="1" LINESP="14.4" ROT="0" TXTSHX="5" TXTSTROKE="Black" WIDTH="135" ImageRes="1" GROUPS="62 " LOCKR="0" IFONT="Bitstream Charter Bold Italic" LOCALSCY="1" NAMEDLST="" TXTSHY="-5" isInline="0" AUTOTEXT="0" FLIPPEDV="0" PCOLOR="None" RADRECT="0" REVERS="0" PRINTABLE="1" RATIO="1" FLIPPEDH="0" COLGAP="0" PCOLOR2="None" NEXTITEM="-1" NUMGROUP="1" TransValue="0" textPathFlipped="0" PLINEEND="0" TXTSTW="-0.1" FRTYPE="0" PTYPE="4" ImageClip="" isTableItem="0" TEXTFLOW2="0" SHADE2="100" TXTBASE="0" PWIDTH="1.03793" HEIGHT="17" DASHOFF="0" PFILE2="" PFILE="" TEXTFLOW3="0" ISIZE="12" textPathType="0" PLTSHOW="0" LINESPMode="0" CLIPEDIT="0" BACKITEM="-1" TransValueS="0" EMBEDDED="1" PFILE3="" ANNAME="" SHADE="100" TXTULP="-0.1" fillRule="1" COCOOR="0 0 0 0 135 0 135 0 135 0 135 0 135 17 135 17 135 17 135 17 0 17 0 17 0 17 0 17 0 0 0 0 " BASEOF="0" PICART="1" COLUMNS="1" TXTKERN="0" OwnPage="0" LAYER="0" BOOKMARK="0" gYpos="12" startArrowIndex="0" TopLine="0" LOCK="0" EPROF="" gXpos="10.924" DASHS="" IRENDER="1" TEXTFLOW="0" TXTFILLSH="100" YPOS="126" TEXTFLOWMODE="0" ANNOTATION="0" LOCALX="0" GRTYP="0" XPOS="136" NUMCO="16" POCOOR="0 0 0 0 135 0 135 0 135 0 135 0 135 17 135 17 135 17 135 17 0 17 0 17 0 17 0 17 0 0 0 0 " EXTRA="1" LOCALY="0" NUMDASH="0" LeftLine="0" PRFILE="" TEXTRA="1" SCALETYPE="1" TXTOUT="1" TXTFILL="Black" endArrowIndex="0" TransBlend="0" BEXTRA="1" PLINEJOIN="0" TXTSTP="-0.1" TXTULW="-0.1" TXTSCALEV="100" TXTSTRSH="100" >
+   <ITEXT SCOLOR="Black" TXTSHX="5" TXTSHY="-5" CH="Agent (Nepomuk feeder)" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <para SCOLOR="Black" ALIGN="0" TXTSHX="5" TXTSHY="-5" TXTSTW="-0.1" KERN="0" FSHADE="100" FEATURES="inherit" PSHORTCUT="" BASEO="0" TXTULP="-0.1" SSHADE="100" SHORTCUT="" SCALEV="100" CNAME="" FONTSIZE="12" NAME="" FCOLOR="Black" SCALEH="100" TXTOUT="1" FONT="Bitstream Charter Bold Italic" TXTSTP="-0.1" TXTULW="-0.1" />
+   <PageItemAttributes/>
+  </PAGEOBJECT>
+ </DOCUMENT>
+</SCRIBUSUTF8NEW>
diff --git a/doc/pics/convert.sh b/doc/pics/convert.sh
new file mode 100755 (executable)
index 0000000..78d9c8d
--- /dev/null
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+for i in Akonadi*.eps; do
+  mv "$i" `echo $i | tr [A-Z] [a-z] | tr [\ ] [_]`;
+done
diff --git a/doc/todo.dox b/doc/todo.dox
new file mode 100644 (file)
index 0000000..ea5bf35
--- /dev/null
@@ -0,0 +1,81 @@
+/**
+
+\page akonadi_todos Junior Jobs & Todos
+
+\section akonadi_jj Junior Jobs
+Jobs that might be easy for beginners to fix. 
+
+<a href="http://bugs.kde.org/buglist.cgi?short_desc_type=regexp&short_desc=%5EJJ%3A.*&product=Akonadi&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&bugidtype=include">
+find JJ in bugs.kde.org
+</a>
+
+
+\section akonadi_bugs Known Issues
+<a href="http://bugs.kde.org/buglist.cgi?short_desc_type=notregexp&short_desc=%28%5EToDo%3A.*%29%7C%28%5EJJ%3A.*%29&product=Akonadi&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&bugidtype=include"> 
+find bugs in bugs.kde.org </a>
+
+
+\section akonadi_todo Todos
+
+There are serveral ways to keep a list of todos up to date. One method is to use
+<TT>\@todo</TT> in comments, those todos will end up on a todo page generated by Doxygen:
+- <a href="./todo.html">akonadi todo list </a>
+- <a href="../server/html/todo.html">server todo list </a>
+- <a href="../libakonadi/html/todo.html">libakonadi todo list </a>
+
+It is not always feasible to place the todo direclty in comments. If the todo is to 
+create a new class where are you going to write your <TT>\@todo</TT>?
+For Akonadi we decided to maintain a public developer Todo list in bugs.kde.org. 
+
+<a href="http://bugs.kde.org/buglist.cgi?short_desc_type=regexp&short_desc=%5EToDo%3A.*&product=Akonadi&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&bugidtype=include">
+find Todos in bugs.kde.org
+</a>
+
+\htmlonly
+<script  type="text/javascript">
+  function showForm() {
+    if ( document.getElementById('new_todo').style.display == 'inline' ) {
+      document.getElementById('new_todo').style.display='none';
+      document.getElementById('toggle_button').innerHTML="Enter new ToDo";
+    } else {
+      document.getElementById('new_todo').style.display='inline';
+      document.getElementById('toggle_button').innerHTML="Hide form";
+    }
+  }
+</script>
+<a id="toggle_button" href="javascript:showForm();">Enter new Todo</a>
+<div id="new_todo" style="display:none">
+  <form id="bugWizard" action="http://bugs.kde.org/wizard.cgi" method="post" name="bugWizard" target=_bugzilla>
+    <input type="hidden" value="fillin" name="step"/>
+    <select style="width: 100%; font-family: monospace;" size="1" name="package">
+      <option value="Akonadi/server">Akonadi/server</option>
+      <option value="Akonadi/libakonadi">Akonadi/libakonadi</option>
+    </select>
+    <input id="shortDesc" type="text" value="Todo: " size="35" style="width: 100%;" name="shortDesc"/>
+    <textarea id="longDesc" style="width: 100%;" wrap="soft" cols="35" rows="5" name="longDesc">
+    </textarea>
+    <input type="hidden" name="severity" value="wishlist">
+    <input type="hidden" value="0" name="kbugreport"/>
+    <input type="hidden" value="" name="os"/>
+    <input type="hidden" value="Devel" name="kdeVersion"/>
+    <input type="hidden" value="" name="appVersion"/>
+    <input type="hidden" value="" name="compiler"/>
+    <input type="hidden" value="" name="package_description"/>
+    <input type="hidden" value="" name="keywords"/>
+    <input id="submit" type="submit" style="width: 7em; height: 2.5em;" value="Finish" name="submit"/>
+  <form>
+</div>
+\endhtmlonly
+
+\par Why keep Todos in bugs.kde.org and not in a Wiki?
+- we have only one place for wishlist, JJ and Todos
+- users interested in the development or looking for a feature can see what we are working on
+- the Todo and the corresponding commit are linked (don't forget \c Bug: in your commit message ;-)
+- we can use the 'owner feature' from bugzilla to see who is working on what
+
+\par What is the difference to a Junior Job?
+JJ are usually more suitable for beginners than Todos.
+If you think you can solve it anyway, take ownership of the todo item by assigning it to yourself or by
+posting a comment. If the bug is already assigned to someone it might be wise to ask first ...
+
+*/
diff --git a/kdepim-mime.xml b/kdepim-mime.xml
new file mode 100644 (file)
index 0000000..7fe1b4c
--- /dev/null
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+It comes with ABSOLUTELY NO WARRANTY, to the extent permitted by law. You may
+redistribute copies of update-mime-database under the terms of the GNU General
+Public License. For more information about these matters, see the file named
+COPYING.
+-->
+<!--
+Notes:
+- the mime types in this file are valid with the version 0.20 of the
+  shared-mime-info package.
+- the "fdo #xxxxx" are the wish in the freedesktop.org bug database to include
+  the mime type there.
+-->
+<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
+  <mime-type type="application/x-vnd.kde.notes">
+    <sub-class-of type="text/plain"/>
+    <comment>KDE "Sticky" Notes</comment>
+  </mime-type>
+  <mime-type type="text/x-vnd.akonadi.note">
+    <sub-class-of type="text/plain"/>
+    <comment>Akonadi Note</comment>
+  </mime-type>
+  <mime-type type="application/x-vnd.akonadi.note">
+    <sub-class-of type="text/plain"/>
+    <comment>Legacy Akonadi Note</comment>
+  </mime-type>
+  <mime-type type="application/x-vnd.kde.alarm">
+    <sub-class-of type="text/calendar"/>
+    <comment>KAlarm Alarm</comment>
+  </mime-type>
+  <mime-type type="application/x-vnd.kde.alarm.active">
+    <sub-class-of type="application/x-vnd.kde.alarm"/>
+    <comment>KAlarm Active Alarm</comment>
+  </mime-type>
+  <mime-type type="application/x-vnd.kde.alarm.archived">
+    <sub-class-of type="application/x-vnd.kde.alarm"/>
+    <comment>KAlarm Archived Alarm</comment>
+  </mime-type>
+  <mime-type type="application/x-vnd.kde.alarm.template">
+    <sub-class-of type="application/x-vnd.kde.alarm"/>
+    <comment>KAlarm Alarm Template</comment>
+  </mime-type>
+</mime-info>
diff --git a/kdepim-runtime-version.h.cmake b/kdepim-runtime-version.h.cmake
new file mode 100644 (file)
index 0000000..76fe15d
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+  Copyright (c) 1998-1999 Preston Brown <pbrown@kde.org>
+  Copyright (c) 2000-2004 Cornelius Schumacher <schumacher@kde.org>
+
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 2 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+/*
+  Set the version for this kdepim-runtime release.
+
+  This version may be used by programs within this module that
+  do not want to maintain a version on their own.
+
+  Note that we cannot use the kdelibs version because we may
+  build against older kdelibs releases.
+*/
+
+#ifndef KDEPIM_RUNTIME_VERSION_H
+#define KDEPIM_RUNTIME_VERSION_H
+
+
+#define KDEPIM_RUNTIME_VERSION "@KDEPIM_RUNTIME_VERSION@"
+
+#endif
diff --git a/kdepim-runtime.categories b/kdepim-runtime.categories
new file mode 100644 (file)
index 0000000..91b1e1a
--- /dev/null
@@ -0,0 +1,18 @@
+log_maildispatcher maildispacher agent (kdepim-runtime)
+log_newmailnotifier mailnotifier agent (kdepim-runtime)
+log_akonadislave kioslave (akonadi)
+log_maildirresource maildir resource (kdepim-runtime)
+log_libmaildir libmaildir (kdepim-runtime)
+log_imapresource imap resource (kdepim-runtime)
+log_mboxresource mbox resource (kdepim-runtime)
+log_birthdays birthdays resource (kdepim-runtime)
+log_davresource dav resource (kdepim-runtime)
+log_kolabresource kolab resource (kdepim-runtime)
+log_mixedmaildirresource mixedmaildir resource (kdepim-runtime)
+log_mixedmaildir mixed maildir resource (kdepim-runtime)
+log_kalarmresource kalarm file resource (kdepim-runtime)
+log_kalarmdirresource kalarm directory resource (kdepim-runtime)
+log_pop3resource pop3 resource (kdepim-runtime)
+log_akonadi_serializer_kalarm akonadi serializer plugins (kdepim-runtime)
+log_akonadi_serializer_mail akonadi mail plugins (kdepim-runtime)
+log_resources_contacts contacts resource (kdepim-runtime)
diff --git a/kioslave/CMakeLists.txt b/kioslave/CMakeLists.txt
new file mode 100644 (file)
index 0000000..ef30c23
--- /dev/null
@@ -0,0 +1,25 @@
+project(kioslave)
+
+set(kio_akonadi_SRCS akonadislave.cpp)
+ecm_qt_declare_logging_category(kio_akonadi_SRCS HEADER akonadislave_debug.h IDENTIFIER AKONADISLAVE_LOG CATEGORY_NAME log_akonadislave)
+
+add_library(kio_akonadi MODULE ${kio_akonadi_SRCS})
+
+
+
+target_link_libraries(kio_akonadi  
+                      KF5::KIOCore 
+                      KF5::AkonadiCore 
+                      KF5::I18n
+                      Qt5::DBus
+                      Qt5::Widgets
+                      )
+set_target_properties(kio_akonadi PROPERTIES OUTPUT_NAME "akonadi")
+
+install(TARGETS kio_akonadi DESTINATION ${KDE_INSTALL_PLUGINDIR}/kf5/kio )
+
+
+########### install files ###############
+
+install( FILES akonadi.protocol  DESTINATION  ${KDE_INSTALL_KSERVICES5DIR} )
+
diff --git a/kioslave/Messages.sh b/kioslave/Messages.sh
new file mode 100644 (file)
index 0000000..acfeb21
--- /dev/null
@@ -0,0 +1,2 @@
+#!/bin/sh
+$XGETTEXT *.cpp -o $podir/kio_akonadi.pot
diff --git a/kioslave/akonadi.protocol b/kioslave/akonadi.protocol
new file mode 100644 (file)
index 0000000..5577928
--- /dev/null
@@ -0,0 +1,10 @@
+[Protocol]
+exec=kf5/kio/akonadi
+protocol=akonadi
+input=none
+output=filesystem
+listing=Name,Type
+reading=true
+writing=false
+deleting=true
+fileNameUsedForCopying=Name
diff --git a/kioslave/akonadislave.cpp b/kioslave/akonadislave.cpp
new file mode 100644 (file)
index 0000000..5144890
--- /dev/null
@@ -0,0 +1,243 @@
+/*
+    Copyright (c) 2006 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "akonadislave.h"
+
+#include <itemfetchjob.h>
+#include <itemfetchscope.h>
+#include <itemdeletejob.h>
+#include <collection.h>
+#include <collectionfetchjob.h>
+#include <collectiondeletejob.h>
+#include <entitydisplayattribute.h>
+
+#include "akonadislave_debug.h"
+
+#include <QApplication>
+#include <KAboutData>
+#include <KLocalizedString>
+#include <QCommandLineParser>
+#include <QCommandLineOption>
+
+extern "C" {
+    int Q_DECL_EXPORT kdemain(int argc, char **argv);
+}
+
+int kdemain(int argc, char **argv)
+{
+    QApplication app(argc, argv);
+    KAboutData aboutData(QStringLiteral("kio_akonadi"), QString(), QStringLiteral("0"));
+    QCommandLineParser parser;
+    KAboutData::setApplicationData(aboutData);
+    parser.addVersionOption();
+    parser.addHelpOption();
+    parser.addOption(QCommandLineOption(QStringList() <<  QStringLiteral("+protocol"), i18n("Protocol name")));
+    parser.addOption(QCommandLineOption(QStringList() <<  QStringLiteral("+pool"), i18n("Socket name")));
+    parser.addOption(QCommandLineOption(QStringList() <<  QStringLiteral("+app"), i18n("Socket name")));
+
+    aboutData.setupCommandLine(&parser);
+    parser.process(app);
+    aboutData.processCommandLine(&parser);
+
+    AkonadiSlave slave(parser.positionalArguments().at(1).toLocal8Bit(), parser.positionalArguments().at(2).toLocal8Bit());
+    slave.dispatchLoop();
+
+    return 0;
+}
+
+using namespace Akonadi;
+
+AkonadiSlave::AkonadiSlave(const QByteArray &pool_socket, const QByteArray &app_socket) :
+    KIO::SlaveBase("akonadi", pool_socket, app_socket)
+{
+    qCDebug(AKONADISLAVE_LOG) << "kio_akonadi starting up";
+}
+
+AkonadiSlave::~ AkonadiSlave()
+{
+    qCDebug(AKONADISLAVE_LOG) << "kio_akonadi shutting down";
+}
+
+void AkonadiSlave::get(const QUrl &url)
+{
+    const Item item = Item::fromUrl(url);
+    ItemFetchJob *job = new ItemFetchJob(item);
+    job->fetchScope().fetchFullPayload();
+
+    if (!job->exec()) {
+        error(KIO::ERR_INTERNAL, job->errorString());
+        return;
+    }
+
+    if (job->items().count() != 1) {
+        error(KIO::ERR_DOES_NOT_EXIST, i18n("No such item."));
+    } else {
+        const Item item = job->items().at(0);
+        QByteArray tmp = item.payloadData();
+        data(tmp);
+        data(QByteArray());
+        finished();
+    }
+
+    finished();
+}
+
+void AkonadiSlave::stat(const QUrl &url)
+{
+    qCDebug(AKONADISLAVE_LOG) << url;
+
+    // Stats for a collection
+    if (Collection::fromUrl(url).isValid()) {
+        Collection collection = Collection::fromUrl(url);
+
+        if (collection != Collection::root()) {
+            // Check that the collection exists.
+            CollectionFetchJob *job = new CollectionFetchJob(collection, CollectionFetchJob::Base);
+            if (!job->exec()) {
+                error(KIO::ERR_INTERNAL, job->errorString());
+                return;
+            }
+
+            if (job->collections().count() != 1) {
+                error(KIO::ERR_DOES_NOT_EXIST, i18n("No such item."));
+                return;
+            }
+
+            collection = job->collections().at(0);
+        }
+
+        statEntry(entryForCollection(collection));
+        finished();
+    }
+    // Stats for an item
+    else if (Item::fromUrl(url).isValid()) {
+        ItemFetchJob *job = new ItemFetchJob(Item::fromUrl(url));
+
+        if (!job->exec()) {
+            error(KIO::ERR_INTERNAL, job->errorString());
+            return;
+        }
+
+        if (job->items().count() != 1) {
+            error(KIO::ERR_DOES_NOT_EXIST, i18n("No such item."));
+            return;
+        }
+
+        const Item item = job->items().at(0);
+        statEntry(entryForItem(item));
+        finished();
+    }
+}
+
+void AkonadiSlave::del(const QUrl &url, bool isFile)
+{
+    qCDebug(AKONADISLAVE_LOG) << url;
+
+    if (!isFile) {                   // It's a directory
+        Collection collection = Collection::fromUrl(url);
+        CollectionDeleteJob *job = new CollectionDeleteJob(collection);
+        if (!job->exec()) {
+            error(KIO::ERR_INTERNAL, job->errorString());
+            return;
+        }
+        finished();
+    } else {                         // It's a file
+        ItemDeleteJob *job = new ItemDeleteJob(Item::fromUrl(url));
+        if (!job->exec()) {
+            error(KIO::ERR_INTERNAL, job->errorString());
+            return;
+        }
+        finished();
+    }
+}
+
+void AkonadiSlave::listDir(const QUrl &url)
+{
+    qCDebug(AKONADISLAVE_LOG) << url;
+
+    if (!Collection::fromUrl(url).isValid()) {
+        error(KIO::ERR_DOES_NOT_EXIST, i18n("No such collection."));
+        return;
+    }
+
+    // Fetching collections
+    Collection collection = Collection::fromUrl(url);
+    if (!collection.isValid()) {
+        error(KIO::ERR_DOES_NOT_EXIST, i18n("No such collection."));
+        return;
+    }
+    CollectionFetchJob *job = new CollectionFetchJob(collection, CollectionFetchJob::FirstLevel);
+    if (!job->exec()) {
+        error(KIO::ERR_CANNOT_ENTER_DIRECTORY, job->errorString());
+        return;
+    }
+
+    Collection::List collections = job->collections();
+    foreach (const Collection &col, collections) {
+        listEntry(entryForCollection(col));
+    }
+
+    // Fetching items
+    if (collection != Collection::root()) {
+        ItemFetchJob *fjob = new ItemFetchJob(collection);
+        if (!fjob->exec()) {
+            error(KIO::ERR_INTERNAL, job->errorString());
+            return;
+        }
+        Item::List items = fjob->items();
+        totalSize(collections.count() + items.count());
+        foreach (const Item &item, items) {
+            listEntry(entryForItem(item));
+        }
+    }
+
+    finished();
+}
+
+KIO::UDSEntry AkonadiSlave::entryForItem(const Akonadi::Item &item)
+{
+    KIO::UDSEntry entry;
+    entry.insert(KIO::UDSEntry::UDS_NAME, QString::number(item.id()));
+    entry.insert(KIO::UDSEntry::UDS_MIME_TYPE, item.mimeType());
+    entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG);
+    entry.insert(KIO::UDSEntry::UDS_URL, item.url().url());
+    entry.insert(KIO::UDSEntry::UDS_SIZE, item.size());
+    entry.insert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IRGRP | S_IROTH);
+    entry.insert(KIO::UDSEntry::UDS_MODIFICATION_TIME, item.modificationTime().toTime_t());
+    return entry;
+}
+
+KIO::UDSEntry AkonadiSlave::entryForCollection(const Akonadi::Collection &collection)
+{
+    KIO::UDSEntry entry;
+    entry.insert(KIO::UDSEntry::UDS_NAME, collection.name());
+    entry.insert(KIO::UDSEntry::UDS_MIME_TYPE, Collection::mimeType());
+    entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
+    entry.insert(KIO::UDSEntry::UDS_URL, collection.url().url());
+    entry.insert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IRGRP | S_IROTH);
+    if (EntityDisplayAttribute *attr = collection.attribute<EntityDisplayAttribute>()) {
+        if (!attr->iconName().isEmpty()) {
+            entry.insert(KIO::UDSEntry::UDS_ICON_NAME, attr->iconName());
+        }
+        if (!attr->displayName().isEmpty()) {
+            entry.insert(KIO::UDSEntry::UDS_DISPLAY_NAME, attr->displayName());
+        }
+    }
+    return entry;
+}
diff --git a/kioslave/akonadislave.h b/kioslave/akonadislave.h
new file mode 100644 (file)
index 0000000..130d4d6
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+    Copyright (c) 2006 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef AKONADI_SLAVE_H
+#define AKONADI_SLAVE_H
+
+#include <kio/slavebase.h>
+
+namespace Akonadi
+{
+class Item;
+class Collection;
+}
+
+class AkonadiSlave : public KIO::SlaveBase
+{
+public:
+    AkonadiSlave(const QByteArray &pool_socket, const QByteArray &app_socket);
+    virtual ~AkonadiSlave();
+
+    /**
+     * Reimplemented from SlaveBase
+     */
+    void get(const QUrl &url) Q_DECL_OVERRIDE;
+
+    /**
+     * Reimplemented from SlaveBase
+     */
+    void stat(const QUrl &url) Q_DECL_OVERRIDE;
+
+    /**
+     * Reimplemented from SlaveBase
+     */
+    void listDir(const QUrl &url) Q_DECL_OVERRIDE;
+
+    /**
+     * Reimplemented from SlaveBase
+     */
+    void del(const QUrl &url, bool isFile) Q_DECL_OVERRIDE;
+
+private:
+    static KIO::UDSEntry entryForItem(const Akonadi::Item &item);
+    static KIO::UDSEntry entryForCollection(const Akonadi::Collection &collection);
+};
+
+#endif
diff --git a/migration/CMakeLists.txt b/migration/CMakeLists.txt
new file mode 100644 (file)
index 0000000..2e01884
--- /dev/null
@@ -0,0 +1,29 @@
+project(migration)
+
+
+add_definitions( -DQT_NO_CAST_FROM_ASCII )
+add_definitions( -DQT_NO_CAST_TO_ASCII )
+#REMOVE IT
+remove_definitions(-DQT_NO_CAST_FROM_BYTEARRAY)
+
+include_directories(
+  ${CMAKE_CURRENT_SOURCE_DIR}/
+)
+
+
+
+# Xsltproc
+find_package(Xsltproc)
+set_package_properties(Xsltproc PROPERTIES DESCRIPTION "XSLT processor from libxslt" TYPE REQUIRED PURPOSE "Required to generate D-Bus interfaces for all Akonadi resources.")
+
+
+set( MIGRATION_AKONADI_SHARED_SOURCES
+  ${CMAKE_CURRENT_SOURCE_DIR}/kmigratorbase.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/infodialog.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/entitytreecreatejob.cpp
+  ${CMAKE_CURRENT_SOURCE_DIR}/migratorbase.cpp
+)
+
+
+add_subdirectory( gid )
+
diff --git a/migration/cmake/FindXsltproc.cmake b/migration/cmake/FindXsltproc.cmake
new file mode 100644 (file)
index 0000000..45b46cf
--- /dev/null
@@ -0,0 +1,32 @@
+# Find xsltproc executable and provide a macro to generate D-Bus interfaces.
+#
+# The following variables are defined :
+# XSLTPROC_EXECUTABLE - path to the xsltproc executable
+# Xsltproc_FOUND - true if the program was found
+#
+find_program(XSLTPROC_EXECUTABLE xsltproc DOC "Path to the xsltproc executable")
+mark_as_advanced(XSLTPROC_EXECUTABLE)
+
+if(XSLTPROC_EXECUTABLE)
+  set(Xsltproc_FOUND TRUE)
+
+  # We depend on kdepimlibs, make sure it's found
+  if(NOT DEFINED KF5Akonadi_DATA_DIR)
+    find_package(KF5Akonadi REQUIRED)
+  endif()
+
+
+  # Macro to generate a D-Bus interface description from a KConfigXT file
+  macro(kcfg_generate_dbus_interface _kcfg _name)
+    add_custom_command(
+      OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${_name}.xml
+      COMMAND ${XSLTPROC_EXECUTABLE} --stringparam interfaceName ${_name}
+      ${KF5Akonadi_DATA_DIR}/kcfg2dbus.xsl
+      ${_kcfg}
+      > ${CMAKE_CURRENT_BINARY_DIR}/${_name}.xml
+      DEPENDS ${KF5Akonadi_DATA_DIR}/kcfg2dbus.xsl
+      ${_kcfg}
+      )
+  endmacro()
+endif()
+
diff --git a/migration/entitytreecreatejob.cpp b/migration/entitytreecreatejob.cpp
new file mode 100644 (file)
index 0000000..ecc9632
--- /dev/null
@@ -0,0 +1,117 @@
+/*
+    Copyright (c) 2010 Stephen Kelly <steveire@gmail.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "entitytreecreatejob.h"
+
+#include <AkonadiCore/CollectionCreateJob>
+#include <AkonadiCore/ItemCreateJob>
+
+#include <QDebug>
+
+using namespace Akonadi;
+
+static const char collectionIdMappingProperty[] = "collectionIdMappingProperty";
+
+EntityTreeCreateJob::EntityTreeCreateJob(const QList< Akonadi::Collection::List > &collections, const Akonadi::Item::List &items, QObject *parent)
+    : Akonadi::TransactionSequence(parent), m_collections(collections), m_items(items), m_pendingJobs(0)
+{
+
+}
+
+void EntityTreeCreateJob::doStart()
+{
+    if (!m_collections.isEmpty()) {
+        createNextLevelOfCollections();
+    }
+    createReadyItems();
+}
+
+void EntityTreeCreateJob::createNextLevelOfCollections()
+{
+    CollectionCreateJob *job;
+
+    const Collection::List colList = m_collections.takeFirst();
+    foreach (const Collection &collection, colList) {
+        ++m_pendingJobs;
+        job = new CollectionCreateJob(collection, this);
+        job->setProperty(collectionIdMappingProperty, collection.id());
+        connect(job, &CollectionCreateJob::result, this, &EntityTreeCreateJob::collectionCreateJobDone);
+    }
+}
+
+void EntityTreeCreateJob::createReadyItems()
+{
+    Item::List::iterator it;
+    for (it = m_items.begin(); it != m_items.end();) {
+        Collection parentCollection = (*it).parentCollection();
+        if (parentCollection.isValid()) {
+            (void) new ItemCreateJob(*it, parentCollection, this);
+            it = m_items.erase(it);
+        } else {
+            ++it;
+        }
+    }
+    if (m_items.isEmpty() && m_collections.isEmpty()) {
+        commit();
+    }
+}
+
+void EntityTreeCreateJob::collectionCreateJobDone(KJob *job)
+{
+    Q_ASSERT(m_pendingJobs > 0);
+    --m_pendingJobs;
+    CollectionCreateJob *collectionCreateJob = qobject_cast<CollectionCreateJob *>(job);
+    Collection createdCollection = collectionCreateJob->collection();
+
+    if (job->error()) {
+        qDebug() << job->errorString();
+        return;
+    }
+
+    const Collection::Id creationId = job->property(collectionIdMappingProperty).toLongLong();
+
+    Item::List::iterator it;
+    const Item::List::iterator end = m_items.end();
+    for (it = m_items.begin(); it != end; ++it) {
+        qDebug() << "updating items";
+        if (it->parentCollection().id() == creationId) {
+            it->setParentCollection(createdCollection);
+        }
+    }
+
+    createReadyItems();
+
+    if (!m_collections.isEmpty()) {
+        Collection::List::iterator col_it;
+        const Collection::List::iterator col_end = m_collections[0].end();
+        for (col_it = m_collections[0].begin(); col_it != col_end; ++col_it) {
+            if (col_it->parentCollection().id() == creationId) {
+                col_it->setParentCollection(createdCollection);
+            }
+        }
+        if (m_pendingJobs == 0) {
+            createNextLevelOfCollections();
+        }
+    }
+
+    if (m_items.isEmpty() && m_collections.isEmpty()) {
+        commit();
+    }
+}
+
diff --git a/migration/entitytreecreatejob.h b/migration/entitytreecreatejob.h
new file mode 100644 (file)
index 0000000..8ec0f01
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+    Copyright (c) 2010 Stephen Kelly <steveire@gmail.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef ENTITYTREECREATEJOB_H
+#define ENTITYTREECREATEJOB_H
+
+#include <AkonadiCore/Job>
+#include <AkonadiCore/Collection>
+#include <AkonadiCore/Item>
+#include <AkonadiCore/TransactionSequence>
+
+class EntityTreeCreateJob : public Akonadi::TransactionSequence
+{
+    Q_OBJECT
+public:
+    explicit EntityTreeCreateJob(const QList<Akonadi::Collection::List> &collections, const Akonadi::Item::List &items, QObject *parent = Q_NULLPTR);
+
+    void doStart() Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void collectionCreateJobDone(KJob *);
+
+private:
+    void createNextLevelOfCollections();
+    void createReadyItems();
+
+private:
+    QList<Akonadi::Collection::List> m_collections;
+    Akonadi::Item::List m_items;
+    int m_pendingJobs;
+};
+
+#endif
diff --git a/migration/gid/CMakeLists.txt b/migration/gid/CMakeLists.txt
new file mode 100644 (file)
index 0000000..2de0273
--- /dev/null
@@ -0,0 +1,24 @@
+set(gid_SRCS
+  gidmigrator.cpp
+  gidmigrationjob.cpp
+  ${MIGRATION_AKONADI_SHARED_SOURCES}
+)
+
+add_library(gidmigration STATIC ${gid_SRCS})
+target_link_libraries(gidmigration
+  KF5::AkonadiCore
+  KF5::Mime
+  KF5::ConfigCore
+  KF5::I18n
+  Qt5::Core
+  Qt5::Widgets
+  KF5::WidgetsAddons
+)
+
+add_executable(gidmigrator main.cpp)
+target_link_libraries(gidmigrator
+  gidmigration
+  KF5::AkonadiWidgets
+  KF5::Mime
+)
+install(TARGETS gidmigrator ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
diff --git a/migration/gid/Messages.sh b/migration/gid/Messages.sh
new file mode 100644 (file)
index 0000000..c49e918
--- /dev/null
@@ -0,0 +1,2 @@
+#!/bin/sh
+$XGETTEXT ../*.cpp *.cpp *.h -o $podir/gid-migrator.pot
diff --git a/migration/gid/gidmigrationjob.cpp b/migration/gid/gidmigrationjob.cpp
new file mode 100644 (file)
index 0000000..7818903
--- /dev/null
@@ -0,0 +1,142 @@
+/*
+    Copyright (c) 2013 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "gidmigrationjob.h"
+#include <AkonadiCore/collectionfetchjob.h>
+#include <AkonadiCore/collectionfetchscope.h>
+#include <AkonadiCore/itemfetchjob.h>
+#include <AkonadiCore/itemmodifyjob.h>
+#include <AkonadiCore/itemfetchscope.h>
+
+using namespace Akonadi;
+
+UpdateJob::UpdateJob(const Collection &col, QObject *parent)
+    :   Job(parent),
+        mCollection(col),
+        mModJobRunning(false)
+{}
+
+UpdateJob::~UpdateJob()
+{}
+
+void UpdateJob::doStart()
+{
+    ItemFetchJob *fetchJob = new ItemFetchJob(mCollection, this);
+    fetchJob->fetchScope().setCacheOnly(true);
+    fetchJob->fetchScope().setIgnoreRetrievalErrors(true);
+    fetchJob->fetchScope().setFetchModificationTime(false);
+    fetchJob->fetchScope().setFetchRemoteIdentification(false);
+    fetchJob->fetchScope().fetchFullPayload(true);
+    //Limit scope to envelope only for mail
+    connect(fetchJob, &ItemFetchJob::itemsReceived, this, &UpdateJob::itemsReceived);
+}
+
+void UpdateJob::itemsReceived(const Akonadi::Item::List &items)
+{
+    //We're queuing items rather than creating ItemModifyJobs directly due to memory concerns
+    //I'm not sure if that would indeed be a problem (a ModifyJob shouldn't be much larger than the item) but we'd have to compare memory usage first when creating large amounts of ItemModifyJobs.
+    foreach (const Akonadi::Item &item, items) {
+        mItemQueue.enqueue(item);
+    }
+    processNext();
+}
+
+void UpdateJob::slotResult(KJob *job)
+{
+    //This slot is automatically called for all subjobs by KCompositeJob
+    //FIXME the fetch job emits result before itemsReceived, because itemsReceived is triggered using the result signal (which is wrong IMO). See ItemFetchJob::timeout
+    //If result was emitted at the end we could avoid having to call processNext in itemsReceived and locking it.
+    ItemFetchJob *const fetchJob = dynamic_cast<ItemFetchJob *>(job);
+    const bool fetchReturnedNoItems = fetchJob && fetchJob->items().isEmpty();
+    Job::slotResult(job);
+    if (fetchReturnedNoItems) {
+        emitResult();
+    } else if (!fetchJob) {
+        mModJobRunning = false;
+        if (!hasSubjobs()) {
+            if (!processNext()) {
+                emitResult();
+            }
+        }
+    }
+}
+
+bool UpdateJob::processNext()
+{
+    if (mModJobRunning || mItemQueue.isEmpty()) {
+        return false;
+    }
+    const Akonadi::Item &item = mItemQueue.dequeue();
+    //Only the single item modifyjob updates the gid
+    ItemModifyJob *modJob = new ItemModifyJob(item, this);
+    modJob->setUpdateGid(true);
+    modJob->setIgnorePayload(true);
+    mModJobRunning = true;
+    return true;
+}
+
+GidMigrationJob::GidMigrationJob(const QStringList &mimeTypeFilter, QObject *parent)
+    :   Job(parent),
+        mMimeTypeFilter(mimeTypeFilter)
+{
+}
+
+GidMigrationJob::~GidMigrationJob()
+{
+}
+
+void GidMigrationJob::doStart()
+{
+    CollectionFetchJob *fetchJob = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, this);
+    fetchJob->fetchScope().setContentMimeTypes(mMimeTypeFilter);
+    connect(fetchJob, &CollectionFetchJob::collectionsReceived, this, &GidMigrationJob::collectionsReceived);
+    connect(fetchJob, &CollectionFetchJob::result, this, &GidMigrationJob::collectionsFetched);
+}
+
+void GidMigrationJob::collectionsReceived(const Collection::List &collections)
+{
+    mCollections << collections;
+}
+
+void GidMigrationJob::collectionsFetched(KJob *job)
+{
+    //Errors are propagated by KCompositeJob
+    if (!job->error()) {
+        processCollection();
+    }
+}
+
+void GidMigrationJob::processCollection()
+{
+    if (mCollections.isEmpty()) {
+        emitResult();
+        return;
+    }
+    const Collection col = mCollections.takeLast();
+    UpdateJob *updateJob = new UpdateJob(col, this);
+    connect(updateJob, &UpdateJob::result, this, &GidMigrationJob::itemsUpdated);
+}
+
+void GidMigrationJob::itemsUpdated(KJob *job)
+{
+    //Errors are propagated by KCompositeJob
+    if (!job->error()) {
+        processCollection();
+    }
+}
diff --git a/migration/gid/gidmigrationjob.h b/migration/gid/gidmigrationjob.h
new file mode 100644 (file)
index 0000000..7900545
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+    Copyright (c) 2013 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef AKONADI_GIDMIGRATIONJOB_H
+#define AKONADI_GIDMIGRATIONJOB_H
+
+#include <AkonadiCore/item.h>
+#include <AkonadiCore/collection.h>
+#include <AkonadiCore/job.h>
+#include <QStringList>
+#include <QQueue>
+
+/**
+ * @short Job that updates the gid of all items in the store.
+ *
+ * Requires a serializer plugin supporting the gidextractor interface for the mimetype of the objects to migrate.
+ *
+ * @author Christian Mollekopf <mollekopf@kolabsys.com>
+ * @since 4.12
+ */
+class GidMigrationJob : public Akonadi::Job
+{
+    Q_OBJECT
+public:
+    /**
+     * @param mimeTypeFilter The list of mimetypes of objects to be migrated.
+     * @param parent The parent object.
+     */
+    explicit GidMigrationJob(const QStringList &mimeTypeFilter, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Destroys the item fetch job.
+     */
+    virtual ~GidMigrationJob();
+
+    void doStart() Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void collectionsReceived(const Akonadi::Collection::List &);
+    void collectionsFetched(KJob *);
+    void itemsUpdated(KJob *);
+
+private:
+    void processCollection();
+    QStringList mMimeTypeFilter;
+    Akonadi::Collection::List mCollections;
+};
+
+/**
+ * @internal
+ */
+class UpdateJob: public Akonadi::Job
+{
+    Q_OBJECT
+public:
+    explicit UpdateJob(const Akonadi::Collection &col, QObject *parent = Q_NULLPTR);
+    virtual ~UpdateJob();
+
+    void doStart() Q_DECL_OVERRIDE;
+    void slotResult(KJob *job) Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void itemsReceived(const Akonadi::Item::List &items);
+private:
+    bool processNext();
+
+    const Akonadi::Collection mCollection;
+    QQueue<Akonadi::Item> mItemQueue;
+    bool mModJobRunning;
+};
+
+#endif
diff --git a/migration/gid/gidmigrator.cpp b/migration/gid/gidmigrator.cpp
new file mode 100644 (file)
index 0000000..28a316a
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+    Copyright (c) 2013 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "gidmigrator.h"
+
+#include <KLocalizedString>
+#include "gidmigrationjob.h"
+
+GidMigrator::GidMigrator(const QString &mimeType)
+    : MigratorBase(QLatin1String("gidmigrator") + mimeType),
+      mMimeType(mimeType)
+{
+}
+
+GidMigrator::~GidMigrator()
+{
+
+}
+
+QString GidMigrator::displayName() const
+{
+    return i18nc("Name of the GID Migrator (intended for advanced users).", "GID Migrator: %1", mMimeType);
+}
+
+QString GidMigrator::description() const
+{
+    return i18n("Ensures that all items with the mimetype %1 have a GID if a GID extractor is available.", mMimeType);
+}
+
+bool GidMigrator::canStart()
+{
+    return MigratorBase::canStart();
+}
+
+bool GidMigrator::shouldAutostart() const
+{
+    return true;
+}
+
+void GidMigrator::startWork()
+{
+    GidMigrationJob *job = new GidMigrationJob(QStringList() << mMimeType, this);
+    connect(job, &GidMigrationJob::result, this, &GidMigrator::migrationFinished);
+}
+
+void GidMigrator::migrationFinished(KJob *job)
+{
+    if (job->error()) {
+        Q_EMIT message(Error, i18n("Migration failed: %1", job->errorString()));
+        setMigrationState(Failed);
+    } else {
+        setMigrationState(Complete);
+    }
+}
diff --git a/migration/gid/gidmigrator.h b/migration/gid/gidmigrator.h
new file mode 100644 (file)
index 0000000..dadcb12
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+    Copyright (c) 2013 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef GIDMIGRATOR_H
+#define GIDMIGRATOR_H
+
+#include <migratorbase.h>
+#include <KJob>
+
+class GidMigrator: public MigratorBase
+{
+    Q_OBJECT
+public:
+    GidMigrator(const QString &mimeType);
+    virtual ~GidMigrator();
+
+    QString displayName() const Q_DECL_OVERRIDE;
+    QString description() const Q_DECL_OVERRIDE;
+
+    bool canStart() Q_DECL_OVERRIDE;
+
+    bool shouldAutostart() const Q_DECL_OVERRIDE;
+
+protected:
+    void startWork() Q_DECL_OVERRIDE;
+private Q_SLOTS:
+    void migrationFinished(KJob *);
+private:
+    QString mMimeType;
+};
+
+#endif // GIDMIGRATOR_H
diff --git a/migration/gid/main.cpp b/migration/gid/main.cpp
new file mode 100644 (file)
index 0000000..03330e9
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+    Copyright (c) 2013 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include <AkonadiWidgets/ControlGui>
+#include <kaboutdata.h>
+
+#include <kconfig.h>
+#include <kconfiggroup.h>
+#include <KLocalizedString>
+
+#include <qdebug.h>
+#include <QIcon>
+#include <QApplication>
+#include <infodialog.h>
+#include <QCommandLineParser>
+#include <QCommandLineOption>
+
+#include "gidmigrator.h"
+
+int main(int argc, char **argv)
+{
+    KLocalizedString::setApplicationDomain("gid-migrator");
+    KAboutData aboutData(QStringLiteral("gid-migrator"),
+                         i18n("GID Migration Tool"),
+                         QStringLiteral("0.1"),
+                         i18n("Migration of Akonadi Items to support GID"),
+                         KAboutLicense::LGPL,
+                         i18n("(c) 2013-2016 the Akonadi developers"),
+                         QStringLiteral("http://pim.kde.org/akonadi/"));
+    aboutData.addAuthor(i18n("Christian Mollekopf"),  i18n("Author"), QStringLiteral("mollekopf@kolabsys.com"));
+
+    QCommandLineParser parser;
+    QApplication app(argc, argv);
+    parser.addVersionOption();
+    parser.addHelpOption();
+    parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("interactive"), i18n("Show reporting dialog")));
+    parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("interactive-on-change"), i18n("Show report only if changes were made")));
+    parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("mimetype"), i18n("MIME type to migrate")));
+
+    aboutData.setupCommandLine(&parser);
+    parser.process(app);
+    aboutData.processCommandLine(&parser);
+
+    app.setQuitOnLastWindowClosed(false);
+    app.setWindowIcon(QIcon::fromTheme(QStringLiteral("akonadi")));
+
+    if (!Akonadi::ControlGui::start(0)) {
+        return 2;
+    }
+
+    InfoDialog *infoDialog = Q_NULLPTR;
+    if (parser.isSet(QStringLiteral("interactive")) || parser.isSet(QStringLiteral("interactive-on-change"))) {
+        infoDialog = new InfoDialog(parser.isSet(QStringLiteral("interactive-on-change")));
+        Akonadi::ControlGui::widgetNeedsAkonadi(infoDialog);
+        infoDialog->show();
+    }
+
+    const QString mimeType = parser.value(QStringLiteral("mimetype"));
+    if (mimeType.isEmpty()) {
+        qWarning() << "set the mimetype to migrate";
+        return 5;
+    }
+
+    GidMigrator *migrator = new GidMigrator(mimeType);
+    if (infoDialog && migrator) {
+        infoDialog->migratorAdded();
+        QObject::connect(migrator, SIGNAL(message(MigratorBase::MessageType,QString)),
+                         infoDialog, SLOT(message(MigratorBase::MessageType,QString)));
+        QObject::connect(migrator, &QObject::destroyed, infoDialog, &InfoDialog::migratorDone);
+        QObject::connect(migrator, SIGNAL(progress(int)), infoDialog, SLOT(progress(int)));
+    }
+    QObject::connect(migrator, SIGNAL(stoppedProcessing()), &app, SLOT(quit));
+    migrator->start();
+    const int result = app.exec();
+    if (InfoDialog::hasError() || migrator->migrationState() == MigratorBase::Failed) {
+        return 3;
+    }
+
+    return result;
+}
diff --git a/migration/infodialog.cpp b/migration/infodialog.cpp
new file mode 100644 (file)
index 0000000..b4f1d57
--- /dev/null
@@ -0,0 +1,193 @@
+/*
+    Copyright (c) 2008 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "infodialog.h"
+
+#include <KCursor>
+#include <QDebug>
+#include <QIcon>
+
+#include <QApplication>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QListWidget>
+#include <QProgressBar>
+#include <QScrollBar>
+#include <QVBoxLayout>
+#include <QDialogButtonBox>
+#include <KConfigGroup>
+#include <QPushButton>
+
+enum {
+    // The max value of the scrollbar. Don't change this without making the kmail
+    // migrator use this. It still uses hardcoded "100".
+    MAX_PROGRESS = 100
+};
+
+bool InfoDialog::mError = false;
+
+InfoDialog::InfoDialog(bool closeWhenDone) :
+    mMigratorCount(0),
+    mChange(false),
+    mCloseWhenDone(closeWhenDone),
+    mAutoScrollList(true)
+{
+    setAttribute(Qt::WA_DeleteOnClose);
+
+    mButtonBox = new QDialogButtonBox(QDialogButtonBox::Close);
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    setLayout(mainLayout);
+    connect(mButtonBox, &QDialogButtonBox::accepted, this, &InfoDialog::accept);
+    connect(mButtonBox, &QDialogButtonBox::rejected, this, &InfoDialog::reject);
+    mButtonBox->button(QDialogButtonBox::Close)->setEnabled(false);
+
+    QWidget *widget = new QWidget(this);
+    QVBoxLayout *widgetLayout = new QVBoxLayout(widget);
+
+    mList = new QListWidget(widget);
+    mList->setMinimumWidth(640);
+    widgetLayout->addWidget(mList);
+
+    QHBoxLayout *statusLayout = new QHBoxLayout;
+    widgetLayout->addLayout(statusLayout);
+
+    mStatusLabel = new QLabel(widget);
+    mStatusLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
+    statusLayout->addWidget(mStatusLabel);
+
+    mProgressBar = new QProgressBar(widget);
+    mProgressBar->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
+    mProgressBar->setMinimumWidth(200);
+    statusLayout->addWidget(mProgressBar);
+    mainLayout->addWidget(widget);
+    mainLayout->addWidget(mButtonBox);
+
+}
+
+InfoDialog::~InfoDialog()
+{
+}
+
+static KMigratorBase::MessageType convertType(MigratorBase::MessageType type)
+{
+    switch (type) {
+    case MigratorBase::Success: return KMigratorBase::Success;
+    case MigratorBase::Error: return KMigratorBase::Error;
+    case MigratorBase::Skip: return KMigratorBase::Skip;
+    case MigratorBase::Warning: return KMigratorBase::Warning;
+    case MigratorBase::Info: return KMigratorBase::Info;
+    }
+    return KMigratorBase::Info;
+}
+
+void InfoDialog::message(MigratorBase::MessageType type, const QString &msg)
+{
+    message(convertType(type), msg);
+}
+
+void InfoDialog::message(KMigratorBase::MessageType type, const QString &msg)
+{
+    bool autoScroll = mAutoScrollList;
+
+    QListWidgetItem *item = new QListWidgetItem(msg, mList);
+    switch (type) {
+    case KMigratorBase::Success:
+        item->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok-apply")));
+        mChange = true;
+        qDebug() << msg;
+        break;
+    case KMigratorBase::Skip:
+        item->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok")));
+        qDebug() << msg;
+        break;
+    case KMigratorBase::Info:
+        item->setIcon(QIcon::fromTheme(QStringLiteral("dialog-information")));
+        qDebug() << msg;
+        break;
+    case KMigratorBase::Warning:
+        item->setIcon(QIcon::fromTheme(QStringLiteral("dialog-warning")));
+        qDebug() << msg;
+        break;
+    case KMigratorBase::Error: {
+        item->setIcon(QIcon::fromTheme(QStringLiteral("dialog-error")));
+        QFont currentFont = font();
+        currentFont.setBold(true);
+        item->setFont(currentFont);
+        mError = true;
+        qCritical() << msg;
+    }
+    break;
+    default:
+        qCritical() << "WTF?";
+    }
+
+    mAutoScrollList = autoScroll;
+
+    if (autoScroll) {
+        mList->scrollToItem(item);
+    }
+}
+
+void InfoDialog::migratorAdded()
+{
+    ++mMigratorCount;
+    QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
+}
+
+void InfoDialog::migratorDone()
+{
+    QApplication::restoreOverrideCursor();
+
+    --mMigratorCount;
+    if (mMigratorCount == 0) {
+        mButtonBox->button(QDialogButtonBox::Close)->setEnabled(true);
+        status(QString());
+        if (mCloseWhenDone && !hasError() && !hasChange()) {
+            accept();
+        }
+    }
+}
+
+void InfoDialog::status(const QString &msg)
+{
+    mStatusLabel->setText(msg);
+    if (msg.isEmpty()) {
+        progress(0, MAX_PROGRESS, MAX_PROGRESS);
+        mProgressBar->setFormat(QString());
+    }
+}
+
+void InfoDialog::progress(int value)
+{
+    mProgressBar->setFormat(QStringLiteral("%p%"));
+    mProgressBar->setValue(value);
+}
+
+void InfoDialog::progress(int min, int max, int value)
+{
+    mProgressBar->setFormat(QStringLiteral("%p%"));
+    mProgressBar->setRange(min, max);
+    mProgressBar->setValue(value);
+}
+
+void InfoDialog::scrollBarMoved(int value)
+{
+    mAutoScrollList = (value == mList->verticalScrollBar()->maximum());
+}
+
diff --git a/migration/infodialog.h b/migration/infodialog.h
new file mode 100644 (file)
index 0000000..f20df26
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+    Copyright (c) 2008 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef INFODIALOG_H
+#define INFODIALOG_H
+
+#include "kmigratorbase.h"
+#include "migratorbase.h"
+#include <QEventLoopLocker>
+#include <QDialog>
+
+class QLabel;
+class QListWidget;
+class QProgressBar;
+class QDialogButtonBox;
+
+class InfoDialog : public QDialog
+{
+    Q_OBJECT
+public:
+    InfoDialog(bool closeWhenDone = true);
+    ~InfoDialog();
+
+public Q_SLOTS:
+    void message(KMigratorBase::MessageType type, const QString &msg);
+    void message(MigratorBase::MessageType type, const QString &msg);
+
+    void migratorAdded();
+    void migratorDone();
+
+    static bool hasError()
+    {
+        return mError;
+    }
+    bool hasChange() const
+    {
+        return mChange;
+    }
+
+    void status(const QString &msg);
+
+    void progress(int value);
+    void progress(int min, int max, int value);
+
+private Q_SLOTS:
+    void scrollBarMoved(int value);
+
+private:
+    QEventLoopLocker eventLoopLocker;
+    QDialogButtonBox *mButtonBox;
+    QListWidget *mList;
+    QLabel *mStatusLabel;
+    QProgressBar *mProgressBar;
+    int mMigratorCount;
+    static bool mError;
+    bool mChange;
+    bool mCloseWhenDone;
+    bool mAutoScrollList;
+};
+
+#endif
diff --git a/migration/kmigratorbase.cpp b/migration/kmigratorbase.cpp
new file mode 100644 (file)
index 0000000..8d9012e
--- /dev/null
@@ -0,0 +1,142 @@
+/*
+    Copyright (c) 2009 Jonathan Armond <jon.armond@gmail.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "kmigratorbase.h"
+
+#include <AkonadiCore/agentinstancecreatejob.h>
+#include <AkonadiCore/agentmanager.h>
+#include <AkonadiCore/agenttype.h>
+
+#include <KSharedConfig>
+#include <KConfigGroup>
+#include <KLocalizedString>
+#include <QDebug>
+#include <QCoreApplication>
+#include <QFile>
+#include <QMetaEnum>
+#include <QTimer>
+#include <QStandardPaths>
+#include <QFileInfo>
+#include <QDir>
+
+using namespace Akonadi;
+
+namespace
+{
+
+QString messageTypeToString(KMigratorBase::MessageType type)
+{
+    switch (type) {
+    case KMigratorBase::Success: return QStringLiteral("Success");
+    case KMigratorBase::Skip:    return QStringLiteral("Skipped");
+    case KMigratorBase::Info:    return QStringLiteral("Info   ");
+    case KMigratorBase::Warning: return QStringLiteral("WARNING");
+    case KMigratorBase::Error:   return QStringLiteral("ERROR  ");
+    }
+    Q_ASSERT(false);
+    return QString();
+}
+
+}
+
+KMigratorBase::KMigratorBase() : m_logFile(0)
+{
+
+    const QString logFileName = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + QCoreApplication::applicationName() + QLatin1String("/migration.log");
+    QFileInfo fileInfo(logFileName);
+    QDir().mkpath(fileInfo.absolutePath());
+
+    m_logFile = new QFile(logFileName);
+    if (!m_logFile->open(QFile::Append)) {
+        delete m_logFile;
+        m_logFile = 0;
+        qWarning() << "Unable to open log file: " << logFileName;
+    }
+    logMessage(Info, QStringLiteral("Starting migration..."));
+    connect(this, &KMigratorBase::message, this, &KMigratorBase::logMessage);
+
+    // load the vtable before we continue
+    QTimer::singleShot(0, this, &KMigratorBase::migrate);
+}
+
+KMigratorBase::~KMigratorBase()
+{
+    logMessage(Info, QStringLiteral("Migration finished."));
+    delete m_logFile;
+}
+
+KMigratorBase::MigrationState KMigratorBase::migrationState(const QString &identifier) const
+{
+    KConfigGroup cfg(KSharedConfig::openConfig(), QStringLiteral("Resource ") + identifier);
+    QMetaEnum e = metaObject()->enumerator(metaObject()->indexOfEnumerator("MigrationState"));
+    const QString s = cfg.readEntry("MigrationState", e.valueToKey(None));
+    MigrationState state = (MigrationState)e.keyToValue(s.toLatin1());
+
+    if (state != None) {
+        const QString resId = cfg.readEntry("ResourceIdentifier", "");
+        // previously migrated but removed again
+        if (!AgentManager::self()->instance(resId).isValid()) {
+            state = None;
+        }
+    }
+
+    return state;
+}
+
+void KMigratorBase::setMigrationState(const QString &identifier, MigrationState state,
+                                      const QString &resId, const QString &type)
+{
+    KConfigGroup cfg(KSharedConfig::openConfig(), QStringLiteral("Resource ") + identifier);
+    QMetaEnum e = metaObject()->enumerator(metaObject()->indexOfEnumerator("MigrationState"));
+    const QString stateStr = QLatin1String(e.valueToKey(state));
+    cfg.writeEntry("MigrationState", stateStr);
+    cfg.writeEntry("ResourceIdentifier", resId);
+    cfg.sync();
+
+    cfg = KConfigGroup(KSharedConfig::openConfig(), "Bridged");
+    QStringList bridgedResources = cfg.readEntry(type + QLatin1String("Resources"), QStringList());
+    if (state == Bridged) {
+        if (!bridgedResources.contains(identifier)) {
+            bridgedResources << identifier;
+        }
+    } else {
+        bridgedResources.removeAll(identifier);
+    }
+    cfg.writeEntry(type + QLatin1String("Resources"), bridgedResources);
+    cfg.sync();
+}
+
+KJob *KMigratorBase::createAgentInstance(const QString &typeId, QObject *receiver, const char *slot)
+{
+    Q_EMIT message(Info, i18n("Creating instance of type %1", typeId));
+    AgentInstanceCreateJob *job = new AgentInstanceCreateJob(typeId, this);
+    connect(job, SIGNAL(result(KJob*)), receiver, slot);
+    job->start();
+    return job;
+}
+
+void KMigratorBase::logMessage(KMigratorBase::MessageType type, const QString &msg)
+{
+    if (m_logFile) {
+        m_logFile->write(QString(QLatin1Char('[') + QDateTime::currentDateTime().toString() + QLatin1String("] ")
+                                 + messageTypeToString(type) + QLatin1String(": ") + msg + QLatin1Char('\n')).toUtf8());
+        m_logFile->flush();
+    }
+}
+
diff --git a/migration/kmigratorbase.h b/migration/kmigratorbase.h
new file mode 100644 (file)
index 0000000..6308e44
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+    Copyright (c) 2009 Jonathan Armond <jon.armond@gmail.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef KMIGRATORBASE_H
+#define KMIGRATORBASE_H
+
+#include <AkonadiCore/agentinstance.h>
+#include <QEventLoopLocker>
+#include <QObject>
+
+class QFile;
+class KJob;
+
+/**
+ * Base class for akonadi resource migrators.
+ */
+class KMigratorBase : public QObject
+{
+    Q_OBJECT
+public:
+    enum MigrationState {
+        None,
+        Bridged,
+        Complete
+    };
+
+    enum MessageType {
+        Success,
+        Skip,
+        Info,
+        Warning,
+        Error
+    };
+
+    Q_ENUMS(MigrationState)
+
+    KMigratorBase();
+    virtual ~KMigratorBase();
+
+    /**
+     * Read resource migration state.
+     *
+     * @return MigrationState and None if the resource with @param identifier as identifier is not available.
+     */
+    MigrationState migrationState(const QString &identifier) const;
+    /**
+     * Set resource migration state.
+     *
+     * Persists migration state in the resource config.
+     * @param resId and @param state is registered under @param identifier.
+     * Additionally all bridged resources are registered in the @param type and @param identifier.
+     */
+    void setMigrationState(const QString &identifier, MigrationState state,
+                           const QString &resId, const QString &type);
+
+    virtual void migrateNext() = 0;
+
+protected:
+    KJob *createAgentInstance(const QString &typeId, QObject *receiver, const char *slot);
+    virtual void migrationFailed(const QString &errorMsg, const Akonadi::AgentInstance &instance
+                                 = Akonadi::AgentInstance()) = 0;
+
+Q_SIGNALS:
+    void message(KMigratorBase::MessageType type, const QString &msg);
+
+protected Q_SLOTS:
+    virtual void migrate() = 0;
+
+private Q_SLOTS:
+    void logMessage(KMigratorBase::MessageType type, const QString &msg);
+
+private:
+    QFile *m_logFile;
+    QEventLoopLocker eventLoopLocker;
+};
+
+#endif
diff --git a/migration/migratorbase.cpp b/migration/migratorbase.cpp
new file mode 100644 (file)
index 0000000..be648af
--- /dev/null
@@ -0,0 +1,291 @@
+/*
+ * Copyright 2013  Christian Mollekopf <mollekopf@kolabsys.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License or (at your option) version 3 or any later version
+ * accepted by the membership of KDE e.V. (or its successor approved
+ * by the membership of KDE e.V.), which shall act as a proxy
+ * defined in Section 14 of version 3 of the license.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "migratorbase.h"
+#include <QDebug>
+#include <KLocalizedString>
+#include <AkonadiCore/servermanager.h>
+#include <QFile>
+#include <QDateTime>
+#include <QStandardPaths>
+#include <QFileInfo>
+#include <QDir>
+#include <QCoreApplication>
+
+static QString messageTypeToString(MigratorBase::MessageType type)
+{
+    switch (type) {
+    case MigratorBase::Success: return QStringLiteral("Success");
+    case MigratorBase::Skip:    return QStringLiteral("Skipped");
+    case MigratorBase::Info:    return QStringLiteral("Info   ");
+    case MigratorBase::Warning: return QStringLiteral("WARNING");
+    case MigratorBase::Error:   return QStringLiteral("ERROR  ");
+    }
+    Q_ASSERT(false);
+    return QString();
+}
+
+static QMap<QString, MigratorBase::MigrationState> fillMigrationStateMapping()
+{
+    QMap<QString, MigratorBase::MigrationState> map;
+    map.insert(QStringLiteral("Complete"), MigratorBase::Complete);
+    map.insert(QStringLiteral("Aborted"), MigratorBase::Aborted);
+    map.insert(QStringLiteral("InProgress"), MigratorBase::InProgress);
+    map.insert(QStringLiteral("Failed"), MigratorBase::Failed);
+    return map;
+}
+
+static QMap<QString, MigratorBase::MigrationState> migrationStateMapping = fillMigrationStateMapping();
+
+static QString stateToIdentifier(MigratorBase::MigrationState state)
+{
+    Q_ASSERT(migrationStateMapping.values().contains(state));
+    return migrationStateMapping.key(state);
+}
+
+static MigratorBase::MigrationState identifierToState(const QString &identifier)
+{
+    Q_ASSERT(migrationStateMapping.contains(identifier));
+    return migrationStateMapping.value(identifier);
+}
+
+MigratorBase::MigratorBase(const QString &identifier, QObject *parent)
+    :   QObject(parent),
+        mIdentifier(identifier),
+        mMigrationState(None),
+        mConfig(new KConfig(Akonadi::ServerManager::addNamespace(QStringLiteral("akonadi-migrationrc"))))
+{
+    const QString logFileName = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + QCoreApplication::applicationName() + QStringLiteral("/") + identifier + QStringLiteral("migration.log");
+    QFileInfo fileInfo(logFileName);
+    QDir().mkpath(fileInfo.absolutePath());
+    setLogfile(logFileName);
+    connect(this, &MigratorBase::message, this, &MigratorBase::logMessage);
+    loadState();
+}
+
+MigratorBase::MigratorBase(const QString &identifier, const QString &configFile, const QString &logFile, QObject *parent)
+    :   QObject(parent),
+        mIdentifier(identifier),
+        mMigrationState(None)
+{
+    if (!configFile.isEmpty()) {
+        mConfig.reset(new KConfig(configFile));
+    }
+    setLogfile(logFile);
+    connect(this, &MigratorBase::message, this, &MigratorBase::logMessage);
+    loadState();
+}
+
+MigratorBase::~MigratorBase()
+{
+
+}
+
+void MigratorBase::setLogfile(const QString &logfile)
+{
+    if (!logfile.isEmpty()) {
+        mLogFile.reset(new QFile(logfile));
+        if (!mLogFile->open(QFile::Append)) {
+            mLogFile.reset();
+            qWarning() << "Unable to open log file: " << logfile;
+        }
+    } else {
+        mLogFile.reset();
+    }
+}
+
+QString MigratorBase::identifier() const
+{
+    return mIdentifier;
+}
+
+QString MigratorBase::displayName() const
+{
+    return QString();
+}
+
+QString MigratorBase::description() const
+{
+    return QString();
+}
+
+QString MigratorBase::logfile() const
+{
+    if (mLogFile) {
+        return mLogFile->fileName();
+    }
+    return QString();
+}
+
+bool MigratorBase::canStart()
+{
+    if (mIdentifier.isEmpty()) {
+        Q_EMIT message(Error, i18n("Missing Identifier"));
+        return false;
+    }
+    return true;
+}
+
+void MigratorBase::start()
+{
+    if (mMigrationState == InProgress) {
+        qWarning() << "already running";
+        return;
+    }
+    if (!canStart()) {
+        Q_EMIT message(Error, i18n("Failed to start migration because migrator is not ready"));
+        Q_EMIT stoppedProcessing();
+        return;
+    }
+    //TODO acquire dbus lock
+    logMessage(Info, displayName());
+    Q_EMIT message(Info, i18n("Starting migration..."));
+    setMigrationState(InProgress);
+    setProgress(0);
+    startWork();
+}
+
+void MigratorBase::pause()
+{
+    qWarning() << "pause is not implemented";
+}
+
+void MigratorBase::resume()
+{
+    qWarning() << "resume is not implemented";
+}
+
+void MigratorBase::abort()
+{
+    qWarning() << "abort is not implemented";
+}
+
+void MigratorBase::logMessage(MigratorBase::MessageType type, const QString &msg)
+{
+    if (mLogFile) {
+        mLogFile->write(QString(QLatin1Char('[') + QDateTime::currentDateTime().toString() + QStringLiteral("] ")
+                                + messageTypeToString(type) + QStringLiteral(": ") + msg + QLatin1Char('\n')).toUtf8());
+        mLogFile->flush();
+    }
+}
+
+bool MigratorBase::shouldAutostart() const
+{
+    return false;
+}
+
+void MigratorBase::setMigrationState(MigratorBase::MigrationState state)
+{
+    mMigrationState = state;
+    switch (state) {
+    case Complete:
+        setProgress(100);
+        Q_EMIT message(Success, i18n("Migration complete"));
+        Q_EMIT stoppedProcessing();
+        break;
+    case Aborted:
+        Q_EMIT message(Skip, i18n("Migration aborted"));
+        Q_EMIT stoppedProcessing();
+        break;
+    case InProgress:
+        break;
+    case Failed:
+        Q_EMIT message(Error, i18n("Migration failed"));
+        Q_EMIT stoppedProcessing();
+        break;
+    case Paused:
+        Q_EMIT message(Info, i18n("Migration paused"));
+        Q_EMIT stateChanged(mMigrationState);
+        return;
+    default:
+        qWarning() << "invalid state " << state;
+        Q_ASSERT(false);
+        return;
+    }
+    saveState();
+    Q_EMIT stateChanged(mMigrationState);
+}
+
+MigratorBase::MigrationState MigratorBase::migrationState() const
+{
+    return mMigrationState;
+}
+
+void MigratorBase::saveState()
+{
+    config().writeEntry(QStringLiteral("MigrationState"), stateToIdentifier(mMigrationState));
+}
+
+void MigratorBase::loadState()
+{
+    const QString state = config().readEntry(QStringLiteral("MigrationState"), QString());
+    if (!state.isEmpty()) {
+        mMigrationState = identifierToState(state);
+    }
+
+    if (mMigrationState == InProgress) {
+        Q_EMIT message(Warning, i18n("This migration has already been started once but was aborted"));
+        mMigrationState = NeedsUpdate;
+    }
+    switch (mMigrationState) {
+    case Complete:
+        mProgress = 100;
+        break;
+    default:
+        mProgress = 0;
+    }
+}
+
+NullableConfigGroup MigratorBase::config()
+{
+    if (mConfig) {
+        return NullableConfigGroup(mConfig->group(mIdentifier));
+    }
+    return NullableConfigGroup();
+}
+
+int MigratorBase::progress() const
+{
+    return mProgress;
+}
+
+void MigratorBase::setProgress(int prog)
+{
+    if (mProgress != prog) {
+        mProgress = prog;
+        Q_EMIT progress(prog);
+    }
+}
+
+QString MigratorBase::status() const
+{
+    switch (mMigrationState) {
+    case None: return i18nc("@info:status", "Not started");
+    case InProgress: return i18nc("@info:status", "Running...");
+    case Complete: return i18nc("@info:status", "Complete");
+    case Aborted: return i18nc("@info:status", "Aborted");
+    case Paused: return i18nc("@info:status", "Paused");
+    case NeedsUpdate: return i18nc("@info:status", "Needs Update");
+    case Failed: return i18nc("@info:status", "Failed");
+    }
+    return QString();
+}
+
diff --git a/migration/migratorbase.h b/migration/migratorbase.h
new file mode 100644 (file)
index 0000000..89f2477
--- /dev/null
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2013  Christian Mollekopf <mollekopf@kolabsys.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License or (at your option) version 3 or any later version
+ * accepted by the membership of KDE e.V. (or its successor approved
+ * by the membership of KDE e.V.), which shall act as a proxy
+ * defined in Section 14 of version 3 of the license.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MIGRATORBASE_H
+#define MIGRATORBASE_H
+
+#include <QtCore/QObject>
+#include <QFile>
+#include <kconfig.h>
+#include <KConfigGroup>
+
+class QFile;
+
+class NullableConfigGroup
+{
+public:
+    NullableConfigGroup()
+    {}
+
+    NullableConfigGroup(const KConfigGroup &grp): mConfigGroup(grp)
+    {}
+
+    KConfigGroup &configGroup()
+    {
+        return mConfigGroup;
+    }
+
+    template <typename T>
+    inline T readEntry(const QString &key, const T &aDefault) const
+    {
+        if (mConfigGroup.isValid()) {
+            return mConfigGroup.readEntry<T>(key, aDefault);
+        }
+        return aDefault;
+    }
+
+    template <typename T>
+    inline void writeEntry(const QString &key, const T &value)
+    {
+        if (mConfigGroup.isValid()) {
+            mConfigGroup.writeEntry<T>(key, value);
+        }
+    }
+private:
+    KConfigGroup mConfigGroup;
+};
+
+/**
+ * Base class for generic migration jobs in akonadi.
+ *
+ * MigrationJobs can be run standalone from commandline using a small wrapper application or using the
+ * Akonadi Migration Agent.
+ *
+ * Each migrator should assign a unique identifier for it's state (this identifier must never change).
+ *
+ * The work done by the migrator may be paused, and the migrator may persist it's state to resume migrations after a reboot.
+ *
+ * TODO: The migrator base ensures that no migrator can be run multiple times by locking it over dbus.
+ *
+ * The status is stored in the akonadi instance config directory, meaning the status is stored per akonadi instance.
+ * This is the only reason why this MigratorBase is currently specific to akonadi migration jobs.
+ */
+class MigratorBase : public QObject
+{
+    Q_OBJECT
+public:
+    /**
+     * Default constructor with default config and logfile
+     */
+    explicit MigratorBase(const QString &identifier, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Constructor that allows to inject a configfile and logfile.
+     *
+     * Pass and empty string to disable config and log.
+     */
+    explicit MigratorBase(const QString &identifier, const QString &configFile, const QString &logFile, QObject *parent = Q_NULLPTR);
+
+    virtual ~MigratorBase();
+
+    QString identifier() const;
+
+    /**
+     * Translated, human readable display name of migrator.
+     */
+    virtual QString displayName() const;
+
+    /**
+     * Translated, human readable description of migrator.
+     */
+    virtual QString description() const;
+
+    /**
+     * Returns the filename of the logfile used by this migrator.
+     *
+     * Returns QString() if there is no logfile set.
+     */
+    QString logfile() const;
+
+    enum MigrationState {
+        None,
+        InProgress,
+        Paused,
+        Complete,
+        NeedsUpdate,
+        Aborted,
+        Failed
+    };
+
+    enum MessageType {
+        Success,
+        Skip,
+        Info,
+        Warning,
+        Error
+    };
+
+    /**
+     * Read migration state.
+     *
+     * @return MigrationState.
+     */
+    MigrationState migrationState() const;
+
+    /**
+     * Return false if this job cannot start (i.e. due to missing dependencies).
+     */
+    virtual bool canStart();
+
+    /**
+     * Mandatory updates that the Migration Agent should autostart should return true
+     */
+    virtual bool shouldAutostart() const;
+
+    /**
+     * Start migration.
+     *
+     * Implement startWork instead.
+     *
+     * Note that this will directly (blocking) call startWork().
+     */
+    void start();
+
+    /**
+     * Pause migration.
+     */
+    virtual void pause();
+
+    /**
+     * Resume migration.
+     */
+    virtual void resume();
+
+    /**
+     * Abort migration.
+     */
+    virtual void abort();
+
+    /**
+     * progress in percent
+     */
+    int progress() const;
+
+    /**
+     * Status
+     */
+    QString status() const;
+
+Q_SIGNALS:
+    //Signal for state changes
+    void stateChanged(MigratorBase::MigrationState);
+
+    //Signal for log window
+    void message(MigratorBase::MessageType type, const QString &msg);
+
+    //Signal for progress bar
+    void progress(int progress);
+
+    //Signal for scheduling. The migrator has finished for some reason (success, failure, ...) and we can forget about it and move on.
+    void stoppedProcessing();
+
+protected:
+    /**
+     * Reimplement to start work.
+     */
+    virtual void startWork() = 0;
+
+    void setMigrationState(MigratorBase::MigrationState state);
+
+    void setProgress(int);
+
+private Q_SLOTS:
+    /**
+     * Logs a message, that appears in the logfile and potentially in a log window.
+     * Do not call this directly. Emit the message signal instead, which is connected to this slot.
+     */
+    void logMessage(MigratorBase::MessageType type, const QString &msg);
+
+private:
+    NullableConfigGroup config();
+    void saveState();
+    void loadState();
+
+    void setLogfile(const QString &);
+
+    const QString mIdentifier;
+    MigrationState mMigrationState;
+    QScopedPointer<QFile> mLogFile;
+    QScopedPointer<KConfig> mConfig;
+    int mProgress;
+};
+
+Q_DECLARE_METATYPE(MigratorBase::MigrationState)
+
+#endif // MIGRATORBASE_H
diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
new file mode 100644 (file)
index 0000000..adccc95
--- /dev/null
@@ -0,0 +1,56 @@
+project(plugins)
+add_definitions( -DQT_NO_CAST_FROM_ASCII )
+add_definitions( -DQT_NO_CAST_TO_ASCII )
+
+
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_serializer_plugins\")
+
+
+set(akonadi_serializer_addressee_PART_SRCS akonadi_serializer_addressee.cpp )
+add_library(akonadi_serializer_addressee MODULE ${SERIALIZER_TYPE} ${akonadi_serializer_addressee_PART_SRCS})
+target_link_libraries(akonadi_serializer_addressee  KF5::Contacts KF5::AkonadiCore KF5::AkonadiContact KF5::I18n)
+install(TARGETS akonadi_serializer_addressee DESTINATION ${KDE_INSTALL_PLUGINDIR})
+
+set(akonadi_serializer_mail_PART_SRCS akonadi_serializer_mail.cpp)
+ecm_qt_declare_logging_category(akonadi_serializer_mail_PART_SRCS HEADER akonadi_serializer_mail_debug.h IDENTIFIER AKONADI_SERIALIZER_MAIL_LOG CATEGORY_NAME log_akonadi_serializer_mail)
+
+add_library(akonadi_serializer_mail MODULE ${SERIALIZER_TYPE} ${akonadi_serializer_mail_PART_SRCS})
+target_link_libraries(akonadi_serializer_mail  KF5::Mime KF5::AkonadiCore KF5::AkonadiMime KF5::AkonadiPrivate Qt5::DBus )
+install(TARGETS akonadi_serializer_mail DESTINATION ${KDE_INSTALL_PLUGINDIR})
+
+set(akonadi_serializer_kcalcore_SRCS akonadi_serializer_kcalcore.cpp)
+add_library(akonadi_serializer_kcalcore MODULE ${SERIALIZER_TYPE} ${akonadi_serializer_kcalcore_SRCS})
+target_link_libraries(akonadi_serializer_kcalcore  KF5::CalendarUtils KF5::CalendarCore KF5::AkonadiCore)
+install(TARGETS akonadi_serializer_kcalcore DESTINATION ${KDE_INSTALL_PLUGINDIR})
+
+set(akonadi_serializer_contactgroup_PART_SRCS akonadi_serializer_contactgroup.cpp )
+add_library(akonadi_serializer_contactgroup MODULE ${SERIALIZER_TYPE} ${akonadi_serializer_contactgroup_PART_SRCS})
+target_link_libraries(akonadi_serializer_contactgroup  KF5::Contacts KF5::AkonadiCore KF5::AkonadiContact KF5::I18n)
+install(TARGETS akonadi_serializer_contactgroup DESTINATION ${KDE_INSTALL_PLUGINDIR})
+
+set(akonadi_serializer_kalarm_SRCS akonadi_serializer_kalarm.cpp kaeventformatter.cpp)
+ecm_qt_declare_logging_category(akonadi_serializer_kalarm_SRCS HEADER akonadi_serializer_kalarm_debug.h IDENTIFIER AKONADI_SERIALIZER_KALARM_LOG CATEGORY_NAME log_akonadi_serializer_kalarm)
+
+add_library(akonadi_serializer_kalarm MODULE ${SERIALIZER_TYPE} ${akonadi_serializer_kalarm_SRCS})
+target_link_libraries(akonadi_serializer_kalarm
+                      KF5::AlarmCalendar
+                      KF5::CalendarCore
+                      KF5::CalendarUtils
+                      KF5::AkonadiCore
+                     )
+install(TARGETS akonadi_serializer_kalarm DESTINATION ${KDE_INSTALL_PLUGINDIR})
+
+########### install files ###############
+
+install( FILES
+  akonadi_serializer_addressee.desktop
+  akonadi_serializer_mail.desktop
+  akonadi_serializer_kcalcore.desktop
+  akonadi_serializer_contactgroup.desktop
+  akonadi_serializer_kalarm.desktop
+DESTINATION ${KDE_INSTALL_DATADIR}/akonadi/plugins/serializer)
+
+if (BUILD_TESTING)
+   add_subdirectory( autotests ) 
+endif()
+
diff --git a/plugins/Messages.sh b/plugins/Messages.sh
new file mode 100644 (file)
index 0000000..0e943dd
--- /dev/null
@@ -0,0 +1,2 @@
+#!/bin/sh
+$XGETTEXT *.cpp -o $podir/akonadi_serializer_plugins.pot
diff --git a/plugins/akonadi_serializer_addressee.cpp b/plugins/akonadi_serializer_addressee.cpp
new file mode 100644 (file)
index 0000000..fc7e32f
--- /dev/null
@@ -0,0 +1,285 @@
+/*
+    Copyright (c) 2007 Till Adam <adam@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "akonadi_serializer_addressee.h"
+
+#include <AkonadiCore/abstractdifferencesreporter.h>
+#include <AkonadiCore/item.h>
+#include <Akonadi/Contact/ContactParts>
+
+#include <kcontacts/addressee.h>
+#include <KLocalizedString>
+#include <QDebug>
+#include <QtCore/qplugin.h>
+
+using namespace Akonadi;
+
+//// ItemSerializerPlugin interface
+
+bool SerializerPluginAddressee::deserialize(Item &item, const QByteArray &label, QIODevice &data, int version)
+{
+    Q_UNUSED(version);
+
+    KContacts::Addressee addr;
+    if (label == Item::FullPayload) {
+        addr = m_converter.parseVCard(data.readAll());
+    } else if (label == Akonadi::ContactPart::Standard) {
+        addr = m_converter.parseVCard(data.readAll());
+
+        // remove pictures and sound
+        addr.setPhoto(KContacts::Picture());
+        addr.setLogo(KContacts::Picture());
+        addr.setSound(KContacts::Sound());
+    } else if (label == Akonadi::ContactPart::Lookup) {
+        const KContacts::Addressee temp = m_converter.parseVCard(data.readAll());
+
+        // copy only uid, name and email addresses
+        addr.setUid(temp.uid());
+        addr.setPrefix(temp.prefix());
+        addr.setGivenName(temp.givenName());
+        addr.setAdditionalName(temp.additionalName());
+        addr.setFamilyName(temp.familyName());
+        addr.setSuffix(temp.suffix());
+        addr.setEmails(temp.emails());
+    } else {
+        return false;
+    }
+
+    if (!addr.isEmpty()) {
+        item.setPayload<KContacts::Addressee>(addr);
+    } else {
+        qWarning() << "Empty addressee object!";
+    }
+
+    return true;
+}
+
+void SerializerPluginAddressee::serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version)
+{
+    Q_UNUSED(version);
+
+    if (label != Item::FullPayload && label != Akonadi::ContactPart::Standard && label != Akonadi::ContactPart::Lookup) {
+        return;
+    }
+
+    if (!item.hasPayload<KContacts::Addressee>()) {
+        return;
+    }
+
+    KContacts::Addressee addr, temp;
+
+    temp = item.payload<KContacts::Addressee>();
+
+    if (label == Item::FullPayload) {
+        addr = temp;
+    } else if (label == Akonadi::ContactPart::Standard) {
+        addr = temp;
+
+        // remove pictures and sound
+        addr.setPhoto(KContacts::Picture());
+        addr.setLogo(KContacts::Picture());
+        addr.setSound(KContacts::Sound());
+    } else if (label == Akonadi::ContactPart::Lookup) {
+        // copy only uid, name and email addresses
+        addr.setUid(temp.uid());
+        addr.setPrefix(temp.prefix());
+        addr.setGivenName(temp.givenName());
+        addr.setAdditionalName(temp.additionalName());
+        addr.setFamilyName(temp.familyName());
+        addr.setSuffix(temp.suffix());
+        addr.setEmails(temp.emails());
+    }
+
+    data.write(m_converter.createVCard(addr));
+}
+
+//// DifferencesAlgorithmInterface interface
+
+static bool compareString(const QString &left, const QString &right)
+{
+    if (left.isEmpty() && right.isEmpty()) {
+        return true;
+    } else {
+        return left == right;
+    }
+}
+
+static QString toString(const KContacts::PhoneNumber &phoneNumber)
+{
+    return phoneNumber.number();
+}
+
+static QString toString(const KContacts::Address &address)
+{
+    return address.toString();
+}
+
+static QString toString(const QString &value)
+{
+    return value;
+}
+
+template <class T>
+static void compareList(Akonadi::AbstractDifferencesReporter *reporter, const QString &id, const QList<T> &left, const QList<T> &right)
+{
+    for (int i = 0; i < left.count(); ++i) {
+        if (!right.contains(left[ i ])) {
+            reporter->addProperty(AbstractDifferencesReporter::AdditionalLeftMode, id, toString(left[ i ]), QString());
+        }
+    }
+
+    for (int i = 0; i < right.count(); ++i) {
+        if (!left.contains(right[ i ])) {
+            reporter->addProperty(AbstractDifferencesReporter::AdditionalRightMode, id, QString(), toString(right[ i ]));
+        }
+    }
+}
+
+template <class T>
+static void compareVector(Akonadi::AbstractDifferencesReporter *reporter, const QString &id, const QVector<T> &left, const QVector<T> &right)
+{
+    for (int i = 0; i < left.count(); ++i) {
+        if (!right.contains(left[ i ])) {
+            reporter->addProperty(AbstractDifferencesReporter::AdditionalLeftMode, id, toString(left[ i ]), QString());
+        }
+    }
+
+    for (int i = 0; i < right.count(); ++i) {
+        if (!left.contains(right[ i ])) {
+            reporter->addProperty(AbstractDifferencesReporter::AdditionalRightMode, id, QString(), toString(right[ i ]));
+        }
+    }
+}
+
+void SerializerPluginAddressee::compare(Akonadi::AbstractDifferencesReporter *reporter,
+                                        const Akonadi::Item &leftItem,
+                                        const Akonadi::Item &rightItem)
+{
+    Q_ASSERT(reporter);
+    Q_ASSERT(leftItem.hasPayload<KContacts::Addressee>());
+    Q_ASSERT(rightItem.hasPayload<KContacts::Addressee>());
+
+    reporter->setLeftPropertyValueTitle(i18n("Changed Contact"));
+    reporter->setRightPropertyValueTitle(i18n("Conflicting Contact"));
+
+    const KContacts::Addressee leftContact = leftItem.payload<KContacts::Addressee>();
+    const KContacts::Addressee rightContact = rightItem.payload<KContacts::Addressee>();
+
+    if (!compareString(leftContact.uid(), rightContact.uid()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KContacts::Addressee::uidLabel(),
+                              leftContact.uid(), rightContact.uid());
+
+    if (!compareString(leftContact.name(), rightContact.name()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KContacts::Addressee::nameLabel(),
+                              leftContact.name(), rightContact.name());
+
+    if (!compareString(leftContact.formattedName(), rightContact.formattedName()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KContacts::Addressee::formattedNameLabel(),
+                              leftContact.formattedName(), rightContact.formattedName());
+
+    if (!compareString(leftContact.familyName(), rightContact.familyName()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KContacts::Addressee::familyNameLabel(),
+                              leftContact.familyName(), rightContact.familyName());
+
+    if (!compareString(leftContact.givenName(), rightContact.givenName()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KContacts::Addressee::givenNameLabel(),
+                              leftContact.givenName(), rightContact.givenName());
+
+    if (!compareString(leftContact.additionalName(), rightContact.additionalName()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KContacts::Addressee::additionalNameLabel(),
+                              leftContact.additionalName(), rightContact.additionalName());
+
+    if (!compareString(leftContact.prefix(), rightContact.prefix()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KContacts::Addressee::prefixLabel(),
+                              leftContact.prefix(), rightContact.prefix());
+
+    if (!compareString(leftContact.suffix(), rightContact.suffix()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KContacts::Addressee::suffixLabel(),
+                              leftContact.suffix(), rightContact.suffix());
+
+    if (!compareString(leftContact.nickName(), rightContact.nickName()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KContacts::Addressee::nickNameLabel(),
+                              leftContact.nickName(), rightContact.nickName());
+
+    if (leftContact.birthday() != rightContact.birthday())
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KContacts::Addressee::birthdayLabel(),
+                              leftContact.birthday().toString(), rightContact.birthday().toString());
+
+    if (!compareString(leftContact.mailer(), rightContact.mailer()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KContacts::Addressee::mailerLabel(),
+                              leftContact.mailer(), rightContact.mailer());
+
+    if (leftContact.timeZone() != rightContact.timeZone())
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KContacts::Addressee::timeZoneLabel(),
+                              leftContact.timeZone().toString(), rightContact.timeZone().toString());
+
+    if (leftContact.geo() != rightContact.geo())
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KContacts::Addressee::geoLabel(),
+                              leftContact.geo().toString(), rightContact.geo().toString());
+
+    if (!compareString(leftContact.title(), rightContact.title()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KContacts::Addressee::titleLabel(),
+                              leftContact.title(), rightContact.title());
+
+    if (!compareString(leftContact.role(), rightContact.role()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KContacts::Addressee::roleLabel(),
+                              leftContact.role(), rightContact.role());
+
+    if (!compareString(leftContact.organization(), rightContact.organization()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KContacts::Addressee::organizationLabel(),
+                              leftContact.organization(), rightContact.organization());
+
+    if (!compareString(leftContact.note(), rightContact.note()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KContacts::Addressee::noteLabel(),
+                              leftContact.note(), rightContact.note());
+
+    if (!compareString(leftContact.productId(), rightContact.productId()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KContacts::Addressee::productIdLabel(),
+                              leftContact.productId(), rightContact.productId());
+
+    if (!compareString(leftContact.sortString(), rightContact.sortString()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KContacts::Addressee::sortStringLabel(),
+                              leftContact.sortString(), rightContact.sortString());
+
+    if (leftContact.secrecy() != rightContact.secrecy()) {
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KContacts::Addressee::secrecyLabel(),
+                              leftContact.secrecy().toString(), rightContact.secrecy().toString());
+    }
+
+    if (leftContact.url() != rightContact.url())
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KContacts::Addressee::urlLabel(),
+                              leftContact.url().url().toDisplayString(), rightContact.url().url().toDisplayString());
+
+    compareList(reporter, i18n("Emails"), leftContact.emails(), rightContact.emails());
+    compareVector(reporter, i18n("Phone Numbers"), leftContact.phoneNumbers(), rightContact.phoneNumbers());
+    compareVector(reporter, i18n("Addresses"), leftContact.addresses(), rightContact.addresses());
+
+    //TODO: logo/photo/custom entries
+}
+
+//// GidExtractorInterface
+
+QString SerializerPluginAddressee::extractGid(const Item &item) const
+{
+    if (!item.hasPayload<KContacts::Addressee>()) {
+        return QString();
+    }
+    return item.payload<KContacts::Addressee>().uid();
+}
+
diff --git a/plugins/akonadi_serializer_addressee.desktop b/plugins/akonadi_serializer_addressee.desktop
new file mode 100644 (file)
index 0000000..592582f
--- /dev/null
@@ -0,0 +1,101 @@
+[Misc]
+Name=Addressee Serializer
+Name[ar]=مسلسل المُرسَل
+Name[bs]=Serializator adresiranog
+Name[ca]=Serialitzador de destinataris
+Name[ca@valencia]=Serialitzador de destinataris
+Name[da]=Serieordning af adressater
+Name[de]=Empfänger-Serialisierung
+Name[el]=Σειριακοποιητής παραληπτών
+Name[en_GB]=Addressee Serialiser
+Name[es]=Serializador de destinatarios
+Name[et]=Aadresside jadasti
+Name[fi]=Vastaanottajaserialisoija
+Name[fr]=Sérialiseur de destinataires
+Name[ga]=Srathóir Seolaithe
+Name[gl]=Serializador de destinatarios
+Name[hu]=Címzettkezelő
+Name[ia]=Divulgator partial pro adresses
+Name[it]=Serializzatore degli indirizzi
+Name[ja]=受信者用シリアライザ
+Name[kk]=Адрес тізбектеуіші
+Name[km]=ម៉ាស៊ីន​បោះពុម្ព​អ្នក​ទទួល​សំបុត្រ
+Name[ko]=주소록 시리얼라이저
+Name[lt]=Adresatų serializatorius
+Name[lv]=Adrešu serializētājs
+Name[nb]=Adressatserialisator
+Name[nds]=Adressaten-Reegmoduul
+Name[ne]=प्रापक मिलानकर्ता
+Name[nl]=Adressenadministratie
+Name[nn]=Adressatserialisator
+Name[pa]=ਐਡਰੈੱਸ ਸੀਰੀਅਲਾਈਜ਼ਰ
+Name[pl]=Szeregowanie adresatów
+Name[pt]=Serializador de Destinatários
+Name[pt_BR]=Serializador de destinatários
+Name[ro]=Serializator destinatar
+Name[ru]=Сохранение контактов
+Name[sk]=Serializátor adresátov
+Name[sl]=Razvrščevalnik naslovnikov v zaporedje
+Name[sr]=Серијализатор адресаната
+Name[sr@ijekavian]=Серијализатор адресаната
+Name[sr@ijekavianlatin]=Serijalizator adresanata
+Name[sr@latin]=Serijalizator adresanata
+Name[sv]=Adressatserialisering
+Name[tr]=Adres Sıralandırıcı
+Name[uk]=Серіалізатор адрес
+Name[x-test]=xxAddressee Serializerxx
+Name[zh_CN]=收信人序列转换器
+Name[zh_TW]=地址序列器
+Comment=An Akonadi serializer plugin for addressee objects
+Comment[ar]=ملحق مسلسل اكوندا لكائنات المُرسَل
+Comment[bs]=Akonadi serializator za adresirane objekte
+Comment[ca]=Un connector de serialització de l'Akonadi pels objectes destinataris
+Comment[ca@valencia]=Un connector de serialització de l'Akonadi pels objectes destinataris
+Comment[da]=Et Akonadi-plugin til serieordning af adressatobjekter
+Comment[de]=Akonadi-Modul zur Serialisierung von Adressobjekten
+Comment[el]=Ένα πρόσθετο σειριακοποιητή του Akonadi για αντικείμενα παραληπτών
+Comment[en_GB]=An Akonadi serialiser plugin for addressee objects
+Comment[es]=Un complemento serializador de Akonadi para objetos destinatario
+Comment[et]=Akonadi aadressiobjektide jadastamisplugin
+Comment[fi]=Akonadi-serialisoijaliitännäinen vastaanottajaobjekteille
+Comment[fr]=Un module externe Akonadi pour la sérialisation des destinataires
+Comment[ga]=Breiseán srathóra Akonadi le haghaidh seolaithe
+Comment[gl]=Engadido de serialización do Akonadi para obxectos destinatario
+Comment[hu]=Akonadi-modul a címzettek kezeléséhez
+Comment[ia]=Un plug-in pro divulgator partial de Akonadi pro objectos de adresses
+Comment[it]=Un'estensione di Akonadi per la serializzazione degli indirizzi
+Comment[ja]=受信者オブジェクトのための Akonadi シリアライザプラグイン
+Comment[kk]=Akonadi адресат нысандарының тізбектеуіш плагині
+Comment[km]=កម្មវិធី​ជំនួយ​កម្មវិធី​បោះពុម្ព Akonadi សម្រាប់​វត្ថុ​អ្នក​ទទួល​សំបុត្រ
+Comment[ko]=주소 객제를 위한 시리얼라이저 플러그인
+Comment[lt]=Akonadi adresatų objektų serializatoriaus papildinys
+Comment[lv]=Akonadi adrešu serializēšanas spraudnis
+Comment[nb]=Et Akonadi programtillegg for serialisering av adressat-objekter
+Comment[nds]=Akonadi-Inreegmoduul för Adressaten
+Comment[ne]=प्रापक वस्तुका लागि एउटा एकोनाडी मिलानकर्ता प्लगइन
+Comment[nl]=Een administratieplug-in voor Akonadi voor adressen
+Comment[nn]=Eit Akonadi-serialisatortillegg for adressatobjekt
+Comment[pa]=ਐਡਰੈੱਸ ਆਬਜੈਕਟਾਂ ਲਈ ਅਕੌਂਡੀ ਸੀਰੀਲਾਈਜ਼ਰ ਪਲੱਗਇਨ
+Comment[pl]=Wtyczka Akonadi do szeregowania obiektów adresata
+Comment[pt]=Um 'plugin' de serialização do Akonadi para os objectos endereçados
+Comment[pt_BR]=Um plugin de serialização do Akonadi para os objetos dos destinatários
+Comment[ro]=Modul de serializare Akonadi pentru obiecte „destinatar”
+Comment[ru]=Модуль сохранения контактов для Akonadi
+Comment[sk]=Plugin serializátora Akonadi pre objekty adresátov
+Comment[sl]=Akonadijev vstavek za razvrščanje predmetov naslovnikov v zaporedje
+Comment[sr]=Аконадијев прикључак серијализатора за објекте адресаната
+Comment[sr@ijekavian]=Аконадијев прикључак серијализатора за објекте адресаната
+Comment[sr@ijekavianlatin]=Akonadijev priključak serijalizatora za objekte adresanata
+Comment[sr@latin]=Akonadijev priključak serijalizatora za objekte adresanata
+Comment[sv]=Ett insticksprogram till Akonadi för serialisering av adressatobjekt
+Comment[tr]=Adres nesneleri için bir Akonadi sıralandırıcısı
+Comment[uk]=Додаток серіалізації Akonadi для об'єктів адресатів
+Comment[x-test]=xxAn Akonadi serializer plugin for addressee objectsxx
+Comment[zh_CN]=对收信人对象进行序列转换的 Akonadi 插件
+Comment[zh_TW]=地址物件的 Akonadi 序列器外掛程式
+
+[Plugin]
+Type=text/vcard,text/directory
+X-Akonadi-Class=legacy;default;KContacts::Addressee;
+X-KDE-Library=akonadi_serializer_addressee
+X-KDE-ClassName=Akonadi::SerializerPluginAddressee
diff --git a/plugins/akonadi_serializer_addressee.h b/plugins/akonadi_serializer_addressee.h
new file mode 100644 (file)
index 0000000..4e13d4e
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+    Copyright (c) 2007 Till Adam <adam@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef __AKONADI_SERIALIZER_ADDRESSEE_H__
+#define __AKONADI_SERIALIZER_ADDRESSEE_H__
+
+#include <QtCore/QObject>
+
+#include <AkonadiCore/differencesalgorithminterface.h>
+#include <AkonadiCore/itemserializerplugin.h>
+#include <AkonadiCore/gidextractorinterface.h>
+#include <kcontacts/vcardconverter.h>
+
+namespace Akonadi
+{
+
+class SerializerPluginAddressee : public QObject,
+    public ItemSerializerPlugin,
+    public DifferencesAlgorithmInterface,
+    public GidExtractorInterface
+{
+    Q_OBJECT
+    Q_INTERFACES(Akonadi::ItemSerializerPlugin)
+    Q_INTERFACES(Akonadi::DifferencesAlgorithmInterface)
+    Q_INTERFACES(Akonadi::GidExtractorInterface)
+    Q_PLUGIN_METADATA(IID "org.kde.akonadi.SerializerPluginAddressee")
+public:
+    bool deserialize(Item &item, const QByteArray &label, QIODevice &data, int version) Q_DECL_OVERRIDE;
+    void serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version) Q_DECL_OVERRIDE;
+
+    void compare(Akonadi::AbstractDifferencesReporter *reporter,
+                 const Akonadi::Item &leftItem,
+                 const Akonadi::Item &rightItem) Q_DECL_OVERRIDE;
+
+    QString extractGid(const Item &item) const Q_DECL_OVERRIDE;
+
+private:
+    KContacts::VCardConverter m_converter;
+};
+
+}
+
+#endif
diff --git a/plugins/akonadi_serializer_contactgroup.cpp b/plugins/akonadi_serializer_contactgroup.cpp
new file mode 100644 (file)
index 0000000..015728c
--- /dev/null
@@ -0,0 +1,137 @@
+/*
+    Copyright (c) 2008 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "akonadi_serializer_contactgroup.h"
+
+#include <AkonadiCore/abstractdifferencesreporter.h>
+#include <Akonadi/Contact/ContactGroupExpandJob>
+#include <AkonadiCore/item.h>
+#include <Akonadi/Contact/ContactParts>
+
+#include <kcontacts/contactgroup.h>
+#include <kcontacts/contactgrouptool.h>
+#include <KLocalizedString>
+
+#include <QtCore/qplugin.h>
+
+using namespace Akonadi;
+
+//// ItemSerializerPlugin interface
+
+bool SerializerPluginContactGroup::deserialize(Item &item, const QByteArray &label, QIODevice &data, int version)
+{
+    Q_UNUSED(label);
+    Q_UNUSED(version);
+
+    KContacts::ContactGroup contactGroup;
+
+    if (!KContacts::ContactGroupTool::convertFromXml(&data, contactGroup)) {
+        // TODO: error reporting
+        return false;
+    }
+
+    item.setPayload<KContacts::ContactGroup>(contactGroup);
+
+    return true;
+}
+
+void SerializerPluginContactGroup::serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version)
+{
+    Q_UNUSED(label);
+    Q_UNUSED(version);
+
+    if (!item.hasPayload<KContacts::ContactGroup>()) {
+        return;
+    }
+
+    KContacts::ContactGroupTool::convertToXml(item.payload<KContacts::ContactGroup>(), &data);
+}
+
+//// DifferencesAlgorithmInterface interface
+
+static bool compareString(const QString &left, const QString &right)
+{
+    if (left.isEmpty() && right.isEmpty()) {
+        return true;
+    } else {
+        return left == right;
+    }
+}
+
+static QString toString(const KContacts::Addressee &contact)
+{
+    return contact.fullEmail();
+}
+
+template <class T>
+static void compareVector(AbstractDifferencesReporter *reporter, const QString &id, const QVector<T> &left, const QVector<T> &right)
+{
+    for (int i = 0; i < left.count(); ++i) {
+        if (!right.contains(left[ i ])) {
+            reporter->addProperty(AbstractDifferencesReporter::AdditionalLeftMode, id, toString(left[ i ]), QString());
+        }
+    }
+
+    for (int i = 0; i < right.count(); ++i) {
+        if (!left.contains(right[ i ])) {
+            reporter->addProperty(AbstractDifferencesReporter::AdditionalRightMode, id, QString(), toString(right[ i ]));
+        }
+    }
+}
+
+void SerializerPluginContactGroup::compare(Akonadi::AbstractDifferencesReporter *reporter,
+        const Akonadi::Item &leftItem,
+        const Akonadi::Item &rightItem)
+{
+    Q_ASSERT(reporter);
+    Q_ASSERT(leftItem.hasPayload<KContacts::ContactGroup>());
+    Q_ASSERT(rightItem.hasPayload<KContacts::ContactGroup>());
+
+    reporter->setLeftPropertyValueTitle(i18n("Changed Contact Group"));
+    reporter->setRightPropertyValueTitle(i18n("Conflicting Contact Group"));
+
+    const KContacts::ContactGroup leftContactGroup = leftItem.payload<KContacts::ContactGroup>();
+    const KContacts::ContactGroup rightContactGroup = rightItem.payload<KContacts::ContactGroup>();
+
+    if (!compareString(leftContactGroup.name(), rightContactGroup.name()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18n("Name"),
+                              leftContactGroup.name(), rightContactGroup.name());
+
+    // using job->exec() is ok here, not a hot path
+    Akonadi::ContactGroupExpandJob *leftJob = new Akonadi::ContactGroupExpandJob(leftContactGroup);
+    leftJob->exec();
+
+    Akonadi::ContactGroupExpandJob *rightJob = new Akonadi::ContactGroupExpandJob(rightContactGroup);
+    rightJob->exec();
+
+    compareVector(reporter, i18n("Member"), leftJob->contacts(), rightJob->contacts());
+}
+
+//// GidExtractorInterface
+
+QString SerializerPluginContactGroup::extractGid(const Item &item) const
+{
+    if (!item.hasPayload<KContacts::ContactGroup>()) {
+        return QString();
+    }
+    return item.payload<KContacts::ContactGroup>().id();
+}
+
+//Q_EXPORT_PLUGIN2( akonadi_serializer_contactgroup, Akonadi::SerializerPluginContactGroup )
+
diff --git a/plugins/akonadi_serializer_contactgroup.desktop b/plugins/akonadi_serializer_contactgroup.desktop
new file mode 100644 (file)
index 0000000..287973a
--- /dev/null
@@ -0,0 +1,99 @@
+[Misc]
+Name=Contact Group Serializer
+Name[ar]=مسلسل مجموعة جهات الإتصال
+Name[bs]=Serializator kontaktne grupe
+Name[ca]=Serialitzador de grup de contactes
+Name[ca@valencia]=Serialitzador de grup de contactes
+Name[da]=Serieordning af kontaktgruppe
+Name[de]=Kontaktgruppen-Serialisierung
+Name[el]=Σειριακοποιητής ομάδας επαφών
+Name[en_GB]=Contact Group Serialiser
+Name[es]=Serializador de grupos de contactos
+Name[et]=Kontaktirühma jadasti
+Name[fi]=Yhteystietoryhmäserialisoija
+Name[fr]=Sérialiseur de groupes de contacts
+Name[ga]=Srathóir Grúpa Teagmhálacha
+Name[gl]=Serializador de grupos de contacto
+Name[hu]=Névjegycsoport-kezelő
+Name[ia]=Divulgator partial de gruppo de contactos
+Name[it]=Serializzatore gruppi di contatti
+Name[ja]=連絡先グループ用シリアライザ
+Name[kk]=Контакт тобын тізбектеуіші
+Name[km]=អ្នក​ដាក់​លេខ​សៀរៀល​ក្រុម​ទំនាក់ទំនង
+Name[ko]=연락처 그룹 시리얼라이저
+Name[lt]=Adresatų grupių serializatorius
+Name[lv]=Kontaktu grupu serializētājs
+Name[nb]=Kontaktgruppeserialisator
+Name[nds]=Kontaktkoppel-Reegmoduul
+Name[nl]=Contactgroepadministratie
+Name[nn]=Kontaktgruppeserialisator
+Name[pa]=ਸੰਪਰਕ ਗਰੁੱਪ ਸੀਰੀਅਲਾਈਜ਼ਰ
+Name[pl]=Szeregowanie grup kontaktów
+Name[pt]=Serializador de Grupos de Contactos
+Name[pt_BR]=Serializador de grupos de contatos
+Name[ro]=Serializator grupuri de contacte
+Name[ru]=Сохранение групп контактов
+Name[sk]=Serializátor skupín kontaktov
+Name[sl]=Razvrščevalnik skupin stikov v zaporedje
+Name[sr]=Серијализатор група контаката
+Name[sr@ijekavian]=Серијализатор група контаката
+Name[sr@ijekavianlatin]=Serijalizator grupa kontakata
+Name[sr@latin]=Serijalizator grupa kontakata
+Name[sv]=Kontaktgruppserialisering
+Name[tr]=Kişi Grubu Sıralandırıcı
+Name[uk]=Серіалізатор груп контактів
+Name[x-test]=xxContact Group Serializerxx
+Name[zh_CN]=联系人分组序列转换器
+Name[zh_TW]=聯絡人群組序列器
+Comment=An Akonadi serializer plugin for contact group objects
+Comment[ar]=ملحق مسلسل اكوندا لكائنات مجموعة الإتصال
+Comment[bs]=Akonadi dodatak serializatora za objekte kontakt grupe
+Comment[ca]=Un connector de serialització de l'Akonadi pels objectes de grup de contactes
+Comment[ca@valencia]=Un connector de serialització de l'Akonadi pels objectes de grup de contactes
+Comment[da]=Et Akonadi-plugin til serieordning af kontaktgruppeobjekter
+Comment[de]=Akonadi-Modul zur Serialisierung von Kontaktgruppen
+Comment[el]=Ένα πρόσθετο σειριακοποιητή Akonadi για αντικείμενα ομάδων επαφών
+Comment[en_GB]=An Akonadi serialiser plugin for contact group objects
+Comment[es]=Un complemento serializador de Akonadi para objetos de grupos de contacto
+Comment[et]=Akonadi kontaktirühma objektide jadastamisplugin
+Comment[fi]=Akonadi-serialisoijaliitännäinen yhteystietoryhmäobjekteille
+Comment[fr]=Un module externe Akonadi pour la sérialisation des groupes de contacts
+Comment[ga]=Breiseán srathóra Akonadi le haghaidh grúpaí teagmhálacha
+Comment[gl]=Engadido de serialización do Akonadi para obxectos de grupo de contacto
+Comment[hu]=Akonadi-modul névjegycsoportok kezeléséhez
+Comment[ia]=Un plug-in pro divulgator partial de Akonadi pro objectos de gruppo de contactos
+Comment[it]=Un'estensione di Akonadi per la serializzazione dei gruppi di contatti
+Comment[ja]=連絡先グループオブジェクトのための Akonadi シリアライザプラグイン
+Comment[kk]=Akonadi контакт тобы нысандарының тізбектеуіш плагині
+Comment[km]=កម្មវិធី​ជំនួយ​អ្នក​ដាក់លេខ​សៀរៀល Akonadi សម្រាប់​វត្ថុ​ក្រុម​ទំនាក់ទំនង
+Comment[ko]=연락처 그룹 객체를 위한 Akonadi 시리얼라이저 플러그인
+Comment[lt]=Akonadi serializatoriaus papildinys adresatų grupių objektams
+Comment[lv]=Akonadi kontaktu grupu  serializēšanas spraudnis
+Comment[nb]=Et Akonadi programtillegg for serialisering av kontaktgruppe-objekter
+Comment[nds]=Akonadi-Inreegmoduul för Kontaktkoppeln
+Comment[nl]=Een administratieplug-in voor Akonadi voor contactgroepobjecten
+Comment[nn]=Eit Akonadi-serialisatortillegg for kontaktgruppeobjekt
+Comment[pa]=ਸੰਪਰਕ ਗਰੁੱਪ ਆਬਜੈਕਟਾਂ ਲਈ ਅਕੌਂਡੀ ਸੀਰੀਲਾਈਜ਼ਰ ਪਲੱਗਇਨ
+Comment[pl]=Wtyczka Akonadi do szeregowania obiektów grup kontaktów
+Comment[pt]=Um 'plugin' de serialização do Akonadi para os objectos de grupos de contactos
+Comment[pt_BR]=Um plugin de serialização do Akonadi para os objetos de grupos de contatos
+Comment[ro]=Modul de serializare Akonadi pentru obiecte „grup de contacte”
+Comment[ru]=Модуль сохранения групп контактов для Akonadi
+Comment[sk]=Plugin serializátora Akonadi pre objekty skupín kontaktov
+Comment[sl]=Akonadijev vstavek za razvrščanje predmetov skupin stikov v zaporedje
+Comment[sr]=Аконадијев прикључак серијализатора за објекте група контаката
+Comment[sr@ijekavian]=Аконадијев прикључак серијализатора за објекте група контаката
+Comment[sr@ijekavianlatin]=Akonadijev priključak serijalizatora za objekte grupa kontakata
+Comment[sr@latin]=Akonadijev priključak serijalizatora za objekte grupa kontakata
+Comment[sv]=Ett insticksprogram till Akonadi för serialisering av kontaktgruppobjekt
+Comment[tr]=Kişi grubu nesneleri için bir Akonadi sıralandırıcısı
+Comment[uk]=Додаток серіалізації Akonadi для об'єктів груп контактів
+Comment[x-test]=xxAn Akonadi serializer plugin for contact group objectsxx
+Comment[zh_CN]=对联系人分组对象进行序列转换的 Akonadi 插件
+Comment[zh_TW]=聯絡人群組物件的 Akonadi 序列器外掛程式
+
+[Plugin]
+Type=application/x-vnd.kde.contactgroup
+X-Akonadi-Class=legacy;default;KContacts::ContactGroup;
+X-KDE-Library=akonadi_serializer_contactgroup
+X-KDE-ClassName=Akonadi::SerializerPluginContactGroup
diff --git a/plugins/akonadi_serializer_contactgroup.h b/plugins/akonadi_serializer_contactgroup.h
new file mode 100644 (file)
index 0000000..6abcc34
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+    Copyright (c) 2008 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef __AKONADI_SERIALIZER_CONTACTGROUP_H__
+#define __AKONADI_SERIALIZER_CONTACTGROUP_H__
+
+#include <QtCore/QObject>
+
+#include <AkonadiCore/differencesalgorithminterface.h>
+#include <AkonadiCore/itemserializerplugin.h>
+#include <AkonadiCore/gidextractorinterface.h>
+
+namespace Akonadi
+{
+
+/**
+ * @since 4.2
+ */
+class SerializerPluginContactGroup : public QObject,
+    public ItemSerializerPlugin,
+    public DifferencesAlgorithmInterface,
+    public GidExtractorInterface
+{
+    Q_OBJECT
+    Q_INTERFACES(Akonadi::ItemSerializerPlugin)
+    Q_INTERFACES(Akonadi::DifferencesAlgorithmInterface)
+    Q_INTERFACES(Akonadi::GidExtractorInterface)
+    Q_PLUGIN_METADATA(IID "org.kde.akonadi.SerializerPluginContactGroup")
+public:
+    bool deserialize(Item &item, const QByteArray &label, QIODevice &data, int version) Q_DECL_OVERRIDE;
+    void serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version) Q_DECL_OVERRIDE;
+
+    void compare(Akonadi::AbstractDifferencesReporter *reporter,
+                 const Akonadi::Item &leftItem,
+                 const Akonadi::Item &rightItem) Q_DECL_OVERRIDE;
+
+    QString extractGid(const Item &item) const Q_DECL_OVERRIDE;
+};
+
+}
+
+#endif
diff --git a/plugins/akonadi_serializer_kalarm.cpp b/plugins/akonadi_serializer_kalarm.cpp
new file mode 100644 (file)
index 0000000..9876ca3
--- /dev/null
@@ -0,0 +1,315 @@
+/*
+ *  akonadi_serializer_kalarm.cpp  -  Akonadi resource serializer for KAlarm
+ *  Copyright © 2009-2012 by David Jarvie <djarvie@kde.org>
+ *
+ *  This library is free software; you can redistribute it and/or modify it
+ *  under the terms of the GNU Library General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or (at your
+ *  option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful, but WITHOUT
+ *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ *  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ *  License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public License
+ *  along with this library; see the file COPYING.LIB.  If not, write to the
+ *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ *  02110-1301, USA.
+ */
+
+#include "akonadi_serializer_kalarm.h"
+#include "kaeventformatter.h"
+#include "akonadi_serializer_kalarm_debug.h"
+
+#include <kalarmcal/eventattribute.h>
+#include <kalarmcal/kacalendar.h>
+#include <kalarmcal/kaevent.h>
+
+#include <AkonadiCore/item.h>
+#include <AkonadiCore/abstractdifferencesreporter.h>
+#include <AkonadiCore/attributefactory.h>
+
+#include <klocale.h>
+
+#include <QtCore/qplugin.h>
+
+using namespace Akonadi;
+using namespace KAlarmCal;
+
+// Convert from backend data stream to a KAEvent, and set it into the item's payload.
+bool SerializerPluginKAlarm::deserialize(Item &item, const QByteArray &label, QIODevice &data, int version)
+{
+    Q_UNUSED(version);
+
+    if (label != Item::FullPayload) {
+        return false;
+    }
+
+    KCalCore::Incidence::Ptr i = mFormat.fromString(QString::fromUtf8(data.readAll()));
+    if (!i) {
+        qCWarning(AKONADI_SERIALIZER_KALARM_LOG) << "Failed to parse incidence!";
+        data.seek(0);
+        qCWarning(AKONADI_SERIALIZER_KALARM_LOG) << QString::fromUtf8(data.readAll());
+        return false;
+    }
+    if (i->type() != KCalCore::Incidence::TypeEvent) {
+        qCWarning(AKONADI_SERIALIZER_KALARM_LOG) << "Incidence with uid" << i->uid() << "is not an Event!";
+        data.seek(0);
+        return false;
+    }
+    KAEvent event(i.staticCast<KCalCore::Event>());
+    const QString mime = CalEvent::mimeType(event.category());
+    if (mime.isEmpty()  ||  !event.isValid()) {
+        qCWarning(AKONADI_SERIALIZER_KALARM_LOG) << "Event with uid" << event.id() << "contains no usable alarms!";
+        data.seek(0);
+        return false;
+    }
+    event.setItemId(item.id());
+
+    // Set additional event data contained in attributes
+    if (mRegistered.isEmpty()) {
+        AttributeFactory::registerAttribute<KAlarmCal::EventAttribute>();
+        mRegistered = QStringLiteral("x");   // set to any non-null string
+    }
+    const EventAttribute dummy;
+    if (item.hasAttribute(dummy.type())) {
+        Attribute *a = item.attribute(dummy.type());
+        if (!a) {
+            qCCritical(AKONADI_SERIALIZER_KALARM_LOG) << "deserialize(): Event with uid" << event.id() << "contains null attribute";
+        } else {
+            EventAttribute *evAttr = dynamic_cast<EventAttribute *>(a);
+            if (!evAttr) {
+                // Registering EventAttribute doesn't work in the serializer
+                // unless the application also registers it. This doesn't
+                // matter unless the application uses KAEvent class.
+                qCCritical(AKONADI_SERIALIZER_KALARM_LOG) << "deserialize(): Event with uid" << event.id() << "contains unknown type EventAttribute (application must call AttributeFactory::registerAttribute())";
+            } else {
+                KAEvent::CmdErrType err = evAttr->commandError();
+                event.setCommandError(err);
+            }
+        }
+    }
+
+    item.setMimeType(mime);
+    item.setPayload<KAEvent>(event);
+    return true;
+}
+
+// Convert an item's KAEvent payload to backend data stream.
+void SerializerPluginKAlarm::serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version)
+{
+    Q_UNUSED(version);
+
+    if (label != Item::FullPayload || !item.hasPayload<KAEvent>()) {
+        return;
+    }
+    const KAEvent e = item.payload<KAEvent>();
+    KCalCore::Event::Ptr kcalEvent(new KCalCore::Event);
+    e.updateKCalEvent(kcalEvent, KAEvent::UID_SET);
+    QByteArray head = "BEGIN:VCALENDAR\nPRODID:";
+    head += KACalendar::icalProductId();
+    head += "\nVERSION:2.0\nX-KDE-KALARM-VERSION:";
+    head += KAEvent::currentCalendarVersionString();
+    head += '\n';
+    data.write(head);
+    data.write(mFormat.toString(kcalEvent.staticCast<KCalCore::Incidence>()).toUtf8());
+    data.write("\nEND:VCALENDAR");
+}
+
+void SerializerPluginKAlarm::compare(AbstractDifferencesReporter *reporter, const Item &left, const Item &right)
+{
+    Q_ASSERT(reporter);
+    Q_ASSERT(left.hasPayload<KAEvent>());
+    Q_ASSERT(right.hasPayload<KAEvent>());
+
+    KAEvent eventL = left.payload<KAEvent>();
+    KAEvent eventR = right.payload<KAEvent>();
+    // Note that event attributes are not included, since they are not part of the payload
+    mValueL = KAEventFormatter(eventL, false);
+    mValueR = KAEventFormatter(eventR, false);
+
+    reporter->setLeftPropertyValueTitle(i18nc("@title:column", "Changed Alarm"));
+    reporter->setRightPropertyValueTitle(i18nc("@title:column", "Conflicting Alarm"));
+
+    reportDifference(reporter, KAEventFormatter::Id);
+    if (eventL.revision() != eventR.revision()) {
+        reportDifference(reporter, KAEventFormatter::Revision);
+    }
+    if (eventL.actionSubType() != eventR.actionSubType()) {
+        reportDifference(reporter, KAEventFormatter::AlarmType);
+    }
+    if (eventL.category() != eventR.category()) {
+        reportDifference(reporter, KAEventFormatter::AlarmCategory);
+    }
+    if (eventL.templateName() != eventR.templateName()) {
+        reportDifference(reporter, KAEventFormatter::TemplateName);
+    }
+    if (eventL.createdDateTime() != eventR.createdDateTime()) {
+        reportDifference(reporter, KAEventFormatter::CreatedTime);
+    }
+    if (eventL.startDateTime() != eventR.startDateTime()) {
+        reportDifference(reporter, KAEventFormatter::StartTime);
+    }
+    if (eventL.templateAfterTime() != eventR.templateAfterTime()) {
+        reportDifference(reporter, KAEventFormatter::TemplateAfterTime);
+    }
+    if (*eventL.recurrence() != *eventR.recurrence()) {
+        reportDifference(reporter, KAEventFormatter::Recurrence);
+    }
+    if (eventL.mainDateTime(true) != eventR.mainDateTime(true)) {
+        reportDifference(reporter, KAEventFormatter::NextRecurrence);
+    }
+    if (eventL.repetition() != eventR.repetition()) {
+        reportDifference(reporter, KAEventFormatter::SubRepetition);
+    }
+    if (eventL.repetition().interval() != eventR.repetition().interval()) {
+        reportDifference(reporter, KAEventFormatter::RepeatInterval);
+    }
+    if (eventL.repetition().count() != eventR.repetition().count()) {
+        reportDifference(reporter, KAEventFormatter::RepeatCount);
+    }
+    if (eventL.nextRepetition() != eventR.nextRepetition()) {
+        reportDifference(reporter, KAEventFormatter::NextRepetition);
+    }
+    if (eventL.holidaysExcluded() != eventR.holidaysExcluded()) {
+        reportDifference(reporter, KAEventFormatter::HolidaysExcluded);
+    }
+    if (eventL.workTimeOnly() != eventR.workTimeOnly()) {
+        reportDifference(reporter, KAEventFormatter::WorkTimeOnly);
+    }
+    if (eventL.lateCancel() != eventR.lateCancel()) {
+        reportDifference(reporter, KAEventFormatter::LateCancel);
+    }
+    if (eventL.autoClose() != eventR.autoClose()) {
+        reportDifference(reporter, KAEventFormatter::AutoClose);
+    }
+    if (eventL.copyToKOrganizer() != eventR.copyToKOrganizer()) {
+        reportDifference(reporter, KAEventFormatter::CopyKOrganizer);
+    }
+    if (eventL.enabled() != eventR.enabled()) {
+        reportDifference(reporter, KAEventFormatter::Enabled);
+    }
+    if (eventL.isReadOnly() != eventR.isReadOnly()) {
+        reportDifference(reporter, KAEventFormatter::ReadOnly);
+    }
+    if (eventL.toBeArchived() != eventR.toBeArchived()) {
+        reportDifference(reporter, KAEventFormatter::Archive);
+    }
+    if (eventL.customProperties() != eventR.customProperties()) {
+        reportDifference(reporter, KAEventFormatter::CustomProperties);
+    }
+    if (eventL.message() != eventR.message()) {
+        reportDifference(reporter, KAEventFormatter::MessageText);
+    }
+    if (eventL.fileName() != eventR.fileName()) {
+        reportDifference(reporter, KAEventFormatter::MessageFile);
+    }
+    if (eventL.fgColour() != eventR.fgColour()) {
+        reportDifference(reporter, KAEventFormatter::FgColour);
+    }
+    if (eventL.bgColour() != eventR.bgColour()) {
+        reportDifference(reporter, KAEventFormatter::BgColour);
+    }
+    if (eventL.font() != eventR.font()) {
+        reportDifference(reporter, KAEventFormatter::Font);
+    }
+    if (eventL.preAction() != eventR.preAction()) {
+        reportDifference(reporter, KAEventFormatter::PreAction);
+    }
+    if ((eventL.extraActionOptions() & KAEvent::CancelOnPreActError) != (eventR.extraActionOptions() & KAEvent::CancelOnPreActError)) {
+        reportDifference(reporter, KAEventFormatter::PreActionCancel);
+    }
+    if ((eventL.extraActionOptions() & KAEvent::DontShowPreActError) != (eventR.extraActionOptions() & KAEvent::DontShowPreActError)) {
+        reportDifference(reporter, KAEventFormatter::PreActionNoError);
+    }
+    if (eventL.postAction() != eventR.postAction()) {
+        reportDifference(reporter, KAEventFormatter::PostAction);
+    }
+    if (eventL.confirmAck() != eventR.confirmAck()) {
+        reportDifference(reporter, KAEventFormatter::ConfirmAck);
+    }
+    if (eventL.kmailSerialNumber() != eventR.kmailSerialNumber()) {
+        reportDifference(reporter, KAEventFormatter::KMailSerial);
+    }
+    if (eventL.beep() != eventR.beep()
+            ||  eventL.speak() != eventR.speak()
+            ||  eventL.audioFile() != eventR.audioFile()) {
+        reportDifference(reporter, KAEventFormatter::Sound);
+    }
+    if (eventL.repeatSound() != eventR.repeatSound()) {
+        reportDifference(reporter, KAEventFormatter::SoundRepeat);
+    }
+    if (eventL.soundVolume() != eventR.soundVolume()) {
+        reportDifference(reporter, KAEventFormatter::SoundVolume);
+    }
+    if (eventL.fadeVolume() != eventR.fadeVolume()) {
+        reportDifference(reporter, KAEventFormatter::SoundFadeVolume);
+    }
+    if (eventL.fadeSeconds() != eventR.fadeSeconds()) {
+        reportDifference(reporter, KAEventFormatter::SoundFadeTime);
+    }
+    if (eventL.reminderMinutes() != eventR.reminderMinutes()) {
+        reportDifference(reporter, KAEventFormatter::Reminder);
+    }
+    if (eventL.reminderOnceOnly() != eventR.reminderOnceOnly()) {
+        reportDifference(reporter, KAEventFormatter::ReminderOnce);
+    }
+    if (eventL.deferred() != eventR.deferred()) {
+        reportDifference(reporter, KAEventFormatter::DeferralType);
+    }
+    if (eventL.deferDateTime() != eventR.deferDateTime()) {
+        reportDifference(reporter, KAEventFormatter::DeferralTime);
+    }
+    if (eventL.deferDefaultMinutes() != eventR.deferDefaultMinutes()) {
+        reportDifference(reporter, KAEventFormatter::DeferDefault);
+    }
+    if (eventL.deferDefaultDateOnly() != eventR.deferDefaultDateOnly()) {
+        reportDifference(reporter, KAEventFormatter::DeferDefaultDate);
+    }
+    if (eventL.command() != eventR.command()) {
+        reportDifference(reporter, KAEventFormatter::Command);
+    }
+    if (eventL.logFile() != eventR.logFile()) {
+        reportDifference(reporter, KAEventFormatter::LogFile);
+    }
+    if (eventL.commandXterm() != eventR.commandXterm()) {
+        reportDifference(reporter, KAEventFormatter::CommandXTerm);
+    }
+    if (eventL.emailSubject() != eventR.emailSubject()) {
+        reportDifference(reporter, KAEventFormatter::EmailSubject);
+    }
+    if (eventL.emailFromId() != eventR.emailFromId()) {
+        reportDifference(reporter, KAEventFormatter::EmailFromId);
+    }
+    if (eventL.emailAddresses() != eventR.emailAddresses()) {
+        reportDifference(reporter, KAEventFormatter::EmailTo);
+    }
+    if (eventL.emailBcc() != eventR.emailBcc()) {
+        reportDifference(reporter, KAEventFormatter::EmailBcc);
+    }
+    if (eventL.emailMessage() != eventR.emailMessage()) {
+        reportDifference(reporter, KAEventFormatter::EmailBody);
+    }
+    if (eventL.emailAttachments() != eventR.emailAttachments()) {
+        reportDifference(reporter, KAEventFormatter::EmailAttachments);
+    }
+
+    KLocale *locale = KLocale::global();
+    reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18nc("@label", "Item revision"),
+                          locale->convertDigits(QString::number(left.revision()), locale->digitSet()),
+                          locale->convertDigits(QString::number(right.revision()), locale->digitSet()));
+}
+
+void SerializerPluginKAlarm::reportDifference(AbstractDifferencesReporter *reporter, KAEventFormatter::Parameter id)
+{
+    if (mValueL.isApplicable(id)  ||  mValueR.isApplicable(id)) {
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, KAEventFormatter::label(id), mValueL.value(id), mValueR.value(id));
+    }
+}
+
+QString SerializerPluginKAlarm::extractGid(const Item &item) const
+{
+    return item.hasPayload<KAEvent>() ? item.payload<KAEvent>().id() : QString();
+}
+
diff --git a/plugins/akonadi_serializer_kalarm.desktop b/plugins/akonadi_serializer_kalarm.desktop
new file mode 100644 (file)
index 0000000..4dd06f5
--- /dev/null
@@ -0,0 +1,94 @@
+[Misc]
+Name=KAlarm Event Serializer
+Name[bs]=KAlarm događajni serializer
+Name[ca]=Serialitzador d'esdeveniments del KAlarm
+Name[ca@valencia]=Serialitzador d'esdeveniments del KAlarm
+Name[da]=Serieordning af KAlarm-hændelser
+Name[de]=Kalarm Ereignis-Serialisierung
+Name[el]=Σειριακοποιητής γεγονότων KAlarm
+Name[en_GB]=KAlarm Event Serialiser
+Name[es]=Serializador de eventos de KAlarm
+Name[et]=KAlarmi sündmuste jadasti
+Name[fi]=KAlarm-tapahtumasarjoittaja
+Name[fr]=Sérialiseur d'évènements KAlarm
+Name[ga]=Srathóir Imeachtaí KAlarm
+Name[gl]=Serializador de actividades de KAlarm
+Name[hu]=KAlarm eseménykezelő
+Name[ia]=Divulgator partial de evento de KAlarm
+Name[it]=Serializzatore degli eventi di KAlarm
+Name[ja]=KAlarm イベント用シリアライザ
+Name[kk]=KAlarm оқиғалар тізбектеуіші
+Name[km]=កម្មវិធី​បោះពុម្ព​ព្រឹត្តិការណ៍ KAlarm
+Name[ko]=KAlarm 이벤트 시리얼라이저
+Name[lt]=KAlarm įvykių serializatorius
+Name[lv]=KAlarm notikumu serializētājs
+Name[nb]=KAlarm hendelsesserialisator
+Name[nds]=KAlarm-Begeefnis-Reegmoduul
+Name[nl]=KAlarm gebeurtenissen in volgorde zetten
+Name[nn]=Serialisator for KAlarm-hendingar
+Name[pa]=ਕੇਅਲਾਰਮ ਈਵੈਂਟ ਸੀਰੀਅਲਾਈਜ਼ਰ
+Name[pl]=Szeregowanie zdarzeń KAlarm
+Name[pt]=Serializador de Eventos do KAlarm
+Name[pt_BR]=Serializador de eventos do KAlarm
+Name[ru]=Сохранение событий KAlarm
+Name[sk]=Serializátor udalostí KAlarm
+Name[sl]=Razvrščevalnik dogodkov KAlarm v zaporedje
+Name[sr]=Серијализатор К‑алармових догађаја
+Name[sr@ijekavian]=Серијализатор К‑алармових догађаја
+Name[sr@ijekavianlatin]=Serijalizator K‑alarmovih događaja
+Name[sr@latin]=Serijalizator K‑alarmovih događaja
+Name[sv]=Kalarm händelseserialisering
+Name[tr]=KAlarm Olay Sıralandırıcı
+Name[uk]=Серіалізатор подій KAlarm
+Name[x-test]=xxKAlarm Event Serializerxx
+Name[zh_CN]=KAlarm 事件序列器
+Name[zh_TW]=KAlarm 事件序列器
+Comment=An Akonadi serializer plugin for KAlarm events
+Comment[bs]=Akonadi serializirajući dodatak za KAlarm događaje
+Comment[ca]=Un connector de serialització de l'Akonadi pels esdeveniments del KAlarm
+Comment[ca@valencia]=Un connector de serialització de l'Akonadi pels esdeveniments del KAlarm
+Comment[da]=Et Akonadi-plugin til serieordning af KAlarm-hændelser
+Comment[de]=Akonadi-Modul zur Serialisierung von KAlarm-Ereignissen
+Comment[el]=Ένα πρόσθετο σειριακοποιητή Akonadi για γεγονότα KAlarm
+Comment[en_GB]=An Akonadi serialiser plugin for KAlarm events
+Comment[es]=Un complemento serializador de Akonadi para eventos de KAlarm
+Comment[et]=Akonadi KAlarmi sündmuste jadastamisplugin
+Comment[fi]=Akonadi-sarjoitusliitännäinen KAlarmin tapahtumille
+Comment[fr]=Un module externe Akonadi pour la sérialisation des évènements KAlarm
+Comment[ga]=Breiseán srathóra Akonadi le haghaidh imeachtaí KAlarm
+Comment[gl]=Un engadido serializador do Akonadi para as actividades do KAlarm
+Comment[hu]=Akonadi modul KAlarm események kezeléséhez
+Comment[ia]=Un plug-in de Akonadi pro divulgar partialmente eventos de alarmas in KAlarm
+Comment[it]=Un'estensione di Akonadi per la serializzazione di eventi di KAlarm
+Comment[ja]=KAlarm のイベントのための Akonadi シリアライザプラグイン
+Comment[kk]=KAlarm оқиғалардың Akonadi тізбектеуіш плагині
+Comment[km]=កម្មវិធី​ជំនួយ​​របស់​កម្មវិធី​បោះពុម្ព Akonadi សម្រាប់​ព្រឹត្តិការណ៍ KAlarm
+Comment[ko]=KAlarm 이벤트를 위한 Akonadi 시리얼라이저 플러그인
+Comment[lt]=Akonadi serializatoriaus papildinys KAlarm įvykiai
+Comment[lv]=Akonadi KAlarm notikumu serializēšanas spraudnis
+Comment[nb]=Et Akonadi programtillegg for serialisering av KAlarm-hendelser
+Comment[nds]=Akonadi-Inreegmoduul för KAlarm-Begeefnissen
+Comment[nl]=Een Akonadi-plugin voor het in volgorde zetten van KAlarm gebeurtenissen
+Comment[nn]=Eit Akonadi-serialisatortillegg for KAlarm-hendingar
+Comment[pl]=Wtyczka Akonadi do szeregowania zdarzeń KAlarm
+Comment[pt]=Um 'plugin' de serialização do Akonadi para os eventos do KAlarm
+Comment[pt_BR]=Um plugin de serialização do Akonadi para eventos do KAlarm
+Comment[ru]=Модуль Akonadi сохранения событий KAlarm
+Comment[sk]=Plugin serializátora Akonadi pre udalosti KAlarm
+Comment[sl]=Akonadijev vstavek za razvrščanje predmetov dogodkov KAlarm v zaporedje
+Comment[sr]=Аконадијев прикључак серијализатора за К‑алармове догађаје
+Comment[sr@ijekavian]=Аконадијев прикључак серијализатора за К‑алармове догађаје
+Comment[sr@ijekavianlatin]=Akonadijev priključak serijalizatora za K‑alarmove događaje
+Comment[sr@latin]=Akonadijev priključak serijalizatora za K‑alarmove događaje
+Comment[sv]=Ett insticksprogram till Akonadi för serialisering av Kalarm-händelser
+Comment[tr]=KAlarm olayları için bir Akonadi sıralandırıcısı
+Comment[uk]=Додаток серіалізації Akonadi для подій KAlarm
+Comment[x-test]=xxAn Akonadi serializer plugin for KAlarm eventsxx
+Comment[zh_CN]=用于操作 KAlarm 事件的 Akonadi 序列插件
+Comment[zh_TW]=KAlarm 事件使用的 Akonadi 序列器外掛程式
+
+[Plugin]
+Type=application/x-vnd.kde.alarm,application/x-vnd.kde.alarm.active,application/x-vnd.kde.alarm.archived,application/x-vnd.kde.alarm.template
+X-Akonadi-Class=default;KAlarmCal::KAEvent;
+X-KDE-Library=akonadi_serializer_kalarm
+X-KDE-ClassName=SerializerPluginKAlarm
diff --git a/plugins/akonadi_serializer_kalarm.h b/plugins/akonadi_serializer_kalarm.h
new file mode 100644 (file)
index 0000000..5aa0526
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ *  akonadi_serializer_kalarm.h  -  Akonadi resource serializer for KAlarm
+ *  Copyright © 2009-2014 by David Jarvie <djarvie@kde.org>
+ *
+ *  This library is free software; you can redistribute it and/or modify it
+ *  under the terms of the GNU Library General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or (at your
+ *  option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful, but WITHOUT
+ *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ *  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ *  License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public License
+ *  along with this library; see the file COPYING.LIB.  If not, write to the
+ *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ *  02110-1301, USA.
+ */
+
+#ifndef AKONADI_SERIALIZER_KALARM_H
+#define AKONADI_SERIALIZER_KALARM_H
+
+#include "kaeventformatter.h"
+
+#include <AkonadiCore/itemserializerplugin.h>
+#include <AkonadiCore/differencesalgorithminterface.h>
+#include <AkonadiCore/gidextractorinterface.h>
+#include <KCalCore/ICalFormat>
+
+#include <QtCore/QObject>
+
+namespace Akonadi
+{
+class Item;
+class AbstractDifferencesReporter;
+}
+
+class SerializerPluginKAlarm : public QObject,
+    public Akonadi::ItemSerializerPlugin,
+    public Akonadi::DifferencesAlgorithmInterface,
+    public Akonadi::GidExtractorInterface
+{
+    Q_OBJECT
+    Q_INTERFACES(Akonadi::ItemSerializerPlugin)
+    Q_INTERFACES(Akonadi::DifferencesAlgorithmInterface)
+    Q_INTERFACES(Akonadi::GidExtractorInterface)
+    Q_PLUGIN_METADATA(IID "org.kde.akonadi.SerializerPluginKAlarm")
+public:
+    bool deserialize(Akonadi::Item &item, const QByteArray &label, QIODevice &data, int version) Q_DECL_OVERRIDE;
+    void serialize(const Akonadi::Item &item, const QByteArray &label, QIODevice &data, int &version) Q_DECL_OVERRIDE;
+    void compare(Akonadi::AbstractDifferencesReporter *, const Akonadi::Item &left, const Akonadi::Item &right) Q_DECL_OVERRIDE;
+    QString extractGid(const Akonadi::Item &item) const Q_DECL_OVERRIDE;
+
+private:
+    void reportDifference(Akonadi::AbstractDifferencesReporter *, KAEventFormatter::Parameter);
+
+    KCalCore::ICalFormat mFormat;
+    KAEventFormatter mValueL;
+    KAEventFormatter mValueR;
+    QString mRegistered;
+};
+
+#endif // AKONADI_SERIALIZER_KALARM_H
+
diff --git a/plugins/akonadi_serializer_kcalcore.cpp b/plugins/akonadi_serializer_kcalcore.cpp
new file mode 100644 (file)
index 0000000..c5675e0
--- /dev/null
@@ -0,0 +1,370 @@
+/*
+    Copyright (c) 2007 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "akonadi_serializer_kcalcore.h"
+
+#include <AkonadiCore/abstractdifferencesreporter.h>
+#include <AkonadiCore/item.h>
+#include <AkonadiCore/collection.h>
+
+#include <KCalCore/Event>
+#include <KCalCore/Todo>
+
+#include <KCalUtils/Stringify>
+
+#include <KLocalizedString>
+
+#include <QDate>
+#include <QtCore/qplugin.h>
+#include <QDebug>
+
+using namespace KCalCore;
+using namespace KCalUtils;
+using namespace Akonadi;
+
+SerializerPluginKCalCore::SerializerPluginKCalCore()
+    : mTimeZones(new ICalTimeZones)
+{
+}
+
+//// ItemSerializerPlugin interface
+
+bool SerializerPluginKCalCore::deserialize(Item &item, const QByteArray &label,
+        QIODevice &data, int version)
+{
+    Q_UNUSED(version);
+
+    if (label != Item::FullPayload) {
+        return false;
+    }
+
+    qint32 type;
+    quint32 magic, incidenceVersion;
+    QDataStream input(&data);
+    input >> magic;
+    input >> incidenceVersion;
+    input >> type;
+    data.seek(0);
+
+    Incidence::Ptr incidence;
+
+    if (magic == IncidenceBase::magicSerializationIdentifier()) {
+        IncidenceBase::Ptr base;
+        switch (static_cast<KCalCore::Incidence::IncidenceType>(type)) {
+        case KCalCore::Incidence::TypeEvent: {
+            base = Event::Ptr(new Event());
+            break;
+        }
+        case KCalCore::Incidence::TypeTodo: {
+            base = Todo::Ptr(new Todo());
+            break;
+        }
+        case KCalCore::Incidence::TypeJournal: {
+            base = Journal::Ptr(new Journal());
+            break;
+        }
+        default:
+            break;
+        }
+        input >> base;
+        incidence = base.staticCast<KCalCore::Incidence>();
+    } else {
+        // Use the old format
+        incidence = mFormat.readIncidence(data.readAll(), mTimeZones.data());
+    }
+
+    if (!incidence) {
+        qWarning() << "Failed to parse incidence! Item id = " << item.id()
+                   << "Storage collection id " << item.storageCollectionId()
+                   << "parentCollectionId = " << item.parentCollection().id();
+        data.seek(0);
+        qWarning() << QString::fromUtf8(data.readAll());
+        return false;
+    }
+
+    item.setPayload(incidence);
+    return true;
+}
+
+void SerializerPluginKCalCore::serialize(const Item &item,
+        const QByteArray &label,
+        QIODevice &data, int &version)
+{
+    Q_UNUSED(version);
+
+    if (label != Item::FullPayload || !item.hasPayload<Incidence::Ptr>()) {
+        return;
+    }
+    Incidence::Ptr i = item.payload<Incidence::Ptr>();
+
+    // Using an env variable for now while testing
+    if (qgetenv("KCALCORE_BINARY_SERIALIZER") == QByteArray("1")) {
+        QDataStream output(&data);
+        IncidenceBase::Ptr base = i;
+        output << base;
+    } else {
+        // ### I guess this can be done without hardcoding stuff
+        data.write("BEGIN:VCALENDAR\nPRODID:-//K Desktop Environment//NONSGML libkcal 4.3//EN\nVERSION:2.0\nX-KDE-ICAL-IMPLEMENTATION-VERSION:1.0\n");
+        data.write(mFormat.toRawString(i));
+        data.write("\nEND:VCALENDAR");
+    }
+}
+
+//// DifferencesAlgorithmInterface
+
+static bool compareString(const QString &left, const QString &right)
+{
+    if (left.isEmpty() && right.isEmpty()) {
+        return true;
+    } else {
+        return left == right;
+    }
+}
+
+static QString toString(const Attendee::Ptr &attendee)
+{
+    return attendee->name() + QLatin1Char('<') + attendee->email() + QLatin1Char('>');
+}
+
+static QString toString(const Alarm::Ptr &)
+{
+    return QString();
+}
+
+/*
+static QString toString( const Incidence::Ptr & )
+{
+  return QString();
+}
+*/
+
+static QString toString(const Attachment::Ptr &)
+{
+    return QString();
+}
+
+static QString toString(const QDate &date)
+{
+    return date.toString();
+}
+
+static QString toString(const KDateTime &dateTime)
+{
+    return dateTime.dateTime().toString();
+}
+
+static QString toString(const QString &str)
+{
+    return str;
+}
+
+static QString toString(bool value)
+{
+    if (value) {
+        return i18n("Yes");
+    } else {
+        return i18n("No");
+    }
+}
+
+template <class C>
+static void compareList(AbstractDifferencesReporter *reporter,
+                        const QString &id,
+                        const C &left,
+                        const C &right)
+{
+    for (typename C::const_iterator it = left.begin(), end = left.end(); it != end; ++it) {
+        if (!right.contains(*it)) {
+            reporter->addProperty(AbstractDifferencesReporter::AdditionalLeftMode, id, toString(*it), QString());
+        }
+    }
+
+    for (typename C::const_iterator it = right.begin(), end = right.end(); it != end; ++it) {
+        if (!left.contains(*it)) {
+            reporter->addProperty(AbstractDifferencesReporter::AdditionalRightMode, id, QString(), toString(*it));
+        }
+    }
+}
+
+static void compareIncidenceBase(AbstractDifferencesReporter *reporter,
+                                 const IncidenceBase::Ptr &left,
+                                 const IncidenceBase::Ptr &right)
+{
+    compareList(reporter, i18n("Attendees"), left->attendees(), right->attendees());
+
+    if (!compareString(left->organizer()->fullName(), right->organizer()->fullName()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18n("Organizer"),
+                              left->organizer()->fullName(), right->organizer()->fullName());
+
+    if (!compareString(left->uid(), right->uid()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18n("UID"),
+                              left->uid(), right->uid());
+
+    if (left->allDay() != right->allDay())
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18n("Is all-day"),
+                              toString(left->allDay()), toString(right->allDay()));
+
+    if (left->hasDuration() != right->hasDuration())
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18n("Has duration"),
+                              toString(left->hasDuration()), toString(right->hasDuration()));
+
+    if (left->duration() != right->duration())
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18n("Duration"),
+                              QString::number(left->duration().asSeconds()), QString::number(right->duration().asSeconds()));
+}
+
+static void compareIncidence(AbstractDifferencesReporter *reporter,
+                             const Incidence::Ptr &left,
+                             const Incidence::Ptr &right)
+{
+    if (!compareString(left->description(), right->description()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18n("Description"),
+                              left->description(), right->description());
+
+    if (!compareString(left->summary(), right->summary()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18n("Summary"),
+                              left->summary(), right->summary());
+
+    if (left->status() != right->status())
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18n("Status"),
+                              Stringify::incidenceStatus(left), Stringify::incidenceStatus(right));
+
+    if (left->secrecy() != right->secrecy())
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18n("Secrecy"),
+                              toString(left->secrecy()), toString(right->secrecy()));
+
+    if (left->priority() != right->priority())
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18n("Priority"),
+                              toString(left->priority()), toString(right->priority()));
+
+    if (!compareString(left->location(), right->location()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18n("Location"),
+                              left->location(), right->location());
+
+    compareList(reporter, i18n("Categories"), left->categories(), right->categories());
+    compareList(reporter, i18n("Alarms"), left->alarms(), right->alarms());
+    compareList(reporter, i18n("Resources"), left->resources(), right->resources());
+    compareList(reporter, i18n("Attachments"), left->attachments(), right->attachments());
+    compareList(reporter, i18n("Exception Dates"), left->recurrence()->exDates(), right->recurrence()->exDates());
+    compareList(reporter, i18n("Exception Times"), left->recurrence()->exDateTimes(), right->recurrence()->exDateTimes());
+    // TODO: recurrence dates and date/times, exrules, rrules
+
+    if (left->created() != right->created())
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode,
+                              i18n("Created"), left->created().toString(), right->created().toString());
+
+    if (!compareString(left->relatedTo(), right->relatedTo()))
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode,
+                              i18n("Related Uid"), left->relatedTo(), right->relatedTo());
+}
+
+static void compareEvent(AbstractDifferencesReporter *reporter,
+                         const Event::Ptr &left,
+                         const Event::Ptr &right)
+{
+
+    if (left->dtStart() != right->dtStart())
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18n("Start time"),
+                              left->dtStart().toString(), right->dtStart().toString());
+
+    if (left->hasEndDate() != right->hasEndDate())
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18n("Has End Date"),
+                              toString(left->hasEndDate()), toString(right->hasEndDate()));
+
+    if (left->dtEnd() != right->dtEnd())
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18n("End Date"),
+                              left->dtEnd().toString(), right->dtEnd().toString());
+
+    // TODO: check transparency
+}
+
+static void compareTodo(AbstractDifferencesReporter *reporter,
+                        const Todo::Ptr &left,
+                        const Todo::Ptr &right)
+{
+    if (left->hasStartDate() != right->hasStartDate())
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18n("Has Start Date"),
+                              toString(left->hasStartDate()), toString(right->hasStartDate()));
+
+    if (left->hasDueDate() != right->hasDueDate())
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18n("Has Due Date"),
+                              toString(left->hasDueDate()), toString(right->hasDueDate()));
+
+    if (left->dtDue() != right->dtDue())
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18n("Due Date"),
+                              left->dtDue().toString(), right->dtDue().toString());
+
+    if (left->hasCompletedDate() != right->hasCompletedDate())
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18n("Has Complete Date"),
+                              toString(left->hasCompletedDate()), toString(right->hasCompletedDate()));
+
+    if (left->percentComplete() != right->percentComplete())
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18n("Complete"),
+                              QString::number(left->percentComplete()), QString::number(right->percentComplete()));
+
+    if (left->completed() != right->completed())
+        reporter->addProperty(AbstractDifferencesReporter::ConflictMode, i18n("Completed"),
+                              toString(left->completed()), toString(right->completed()));
+}
+
+void SerializerPluginKCalCore::compare(Akonadi::AbstractDifferencesReporter *reporter,
+                                       const Akonadi::Item &leftItem,
+                                       const Akonadi::Item &rightItem)
+{
+    Q_ASSERT(reporter);
+    Q_ASSERT(leftItem.hasPayload<Incidence::Ptr>());
+    Q_ASSERT(rightItem.hasPayload<Incidence::Ptr>());
+
+    const Incidence::Ptr leftIncidencePtr = leftItem.payload<Incidence::Ptr>();
+    const Incidence::Ptr rightIncidencePtr = rightItem.payload<Incidence::Ptr>();
+
+    if (leftIncidencePtr->type() == Incidence::TypeEvent) {
+        reporter->setLeftPropertyValueTitle(i18n("Changed Event"));
+        reporter->setRightPropertyValueTitle(i18n("Conflicting Event"));
+    } else if (leftIncidencePtr->type() == Incidence::TypeTodo) {
+        reporter->setLeftPropertyValueTitle(i18n("Changed Todo"));
+        reporter->setRightPropertyValueTitle(i18n("Conflicting Todo"));
+    }
+
+    compareIncidenceBase(reporter, leftIncidencePtr, rightIncidencePtr);
+    compareIncidence(reporter, leftIncidencePtr, rightIncidencePtr);
+
+    const Event::Ptr leftEvent = leftIncidencePtr.dynamicCast<Event>();
+    const Event::Ptr rightEvent = rightIncidencePtr.dynamicCast<Event>();
+    if (leftEvent && rightEvent) {
+        compareEvent(reporter, leftEvent, rightEvent);
+    } else {
+        const Todo::Ptr leftTodo =  leftIncidencePtr.dynamicCast<Todo>();
+        const Todo::Ptr rightTodo = rightIncidencePtr.dynamicCast<Todo>();
+        if (leftTodo && rightTodo) {
+            compareTodo(reporter, leftTodo, rightTodo);
+        }
+    }
+}
+
+//// GidExtractorInterface
+
+QString SerializerPluginKCalCore::extractGid(const Item &item) const
+{
+    if (!item.hasPayload<Incidence::Ptr>()) {
+        return QString();
+    }
+    return item.payload<Incidence::Ptr>()->instanceIdentifier();
+}
+
diff --git a/plugins/akonadi_serializer_kcalcore.desktop b/plugins/akonadi_serializer_kcalcore.desktop
new file mode 100644 (file)
index 0000000..155058e
--- /dev/null
@@ -0,0 +1,100 @@
+[Misc]
+Name=Incidence Serializer
+Name[ar]=مسلسل الحدث
+Name[bs]=Seralizator događaja
+Name[ca]=Serialitzador d'incidències
+Name[ca@valencia]=Serialitzador d'incidències
+Name[da]=Serieordning af hændelser
+Name[de]=Ereignis-Serialisierung
+Name[el]=Σειριακοποιητής περιστατικών
+Name[en_GB]=Incidence Serialiser
+Name[es]=Serializador de incidencias
+Name[et]=Sündmuste jadasti
+Name[fi]=Merkintäserialisoija
+Name[fr]=Sérialiseur d'évènements
+Name[ga]=Srathóir Imeachtaí
+Name[gl]=Serializador de incidencias
+Name[hu]=Eseménykezelő
+Name[ia]=Divulgator partial de incidentias
+Name[it]=Serializzatore occorrenze
+Name[ja]=イベント用シリアライザ
+Name[kk]=Істерді тізбектеуіші
+Name[km]=Incidence Serializer
+Name[ko]=사건 시리얼라이저
+Name[lt]=Įvykių serializatorius
+Name[lv]=Notikumu serializētājs
+Name[nb]=Forekomst-serialisering
+Name[nds]=Begeefnis-Reegmoduul
+Name[ne]=घटना मिलानकर्ता
+Name[nl]=Agenda-item-administratie
+Name[nn]=Hendingsserialisator
+Name[pl]=Szeregowanie zdarzeń
+Name[pt]=Serializador de Incidências
+Name[pt_BR]=Serializador de incidências
+Name[ro]=Serializator incidență
+Name[ru]=Сохранение событий
+Name[sk]=Serializátor výskytu
+Name[sl]=Razvrščevalnik pojavitev v zaporedje
+Name[sr]=Серијализатор случајева
+Name[sr@ijekavian]=Серијализатор случајева
+Name[sr@ijekavianlatin]=Serijalizator slučajeva
+Name[sr@latin]=Serijalizator slučajeva
+Name[sv]=Förekomstserialisering
+Name[tr]=Olay Sıralandırıcısı
+Name[uk]=Серіалізатор подій
+Name[x-test]=xxIncidence Serializerxx
+Name[zh_CN]=事件序列转换器
+Name[zh_TW]=頻率序列器
+Comment=An Akonadi serializer plugin for events, tasks and journal entries
+Comment[ar]=ملحق مسلسل اكوندا لمدخلات الأحداث و المهمات و السّجل اليومي
+Comment[bs]=Akonadi dodatak serializatora za događaje, zadatke i žurnal upise
+Comment[ca]=Un connector de serialització de l'Akonadi pels objectes d'incidències
+Comment[ca@valencia]=Un connector de serialització de l'Akonadi pels objectes d'incidències
+Comment[da]=Et Akonadi-plugin til serieordning af hændelser, opgaver og journalindgange
+Comment[de]=Akonadi-Modul zur Serialisierung von Ereignissen
+Comment[el]=Ένα πρόσθετο σειριακοποιητή Akonadi για γεγονότα, εργασίες και καταχωρήσεις χρονικού
+Comment[en_GB]=An Akonadi serialiser plugin for events, tasks and journal entries
+Comment[es]=Un complemento serializador de Akonadi para eventos, tareas y entradas del diario
+Comment[et]=Akonadi sündmuste, ülesannete ja päevikusissekannate jadastamisplugin
+Comment[fi]=Akonadi-serialisoijaliitännäinen tapahtumia, tehtäviä ja päiväkirjamerkintöjä varten.
+Comment[fr]=Un module externe Akonadi pour la sérialisation des évènements
+Comment[ga]=Breiseán srathóra Akonadi le haghaidh imeachtaí, tascanna, agus iontrálacha dialainne
+Comment[gl]=Un complemento de serialización do Akonadi para actividades, tarefas e entradas do diario
+Comment[hu]=Akonadi-modul események, feladatok és naplóbejegyzések kezeléséhez
+Comment[ia]=Un plug-in pro divulgator partial de Akonadi pro eventos, cargas e jornales
+Comment[it]=Un'estensione di Akonadi per la serializzazione di eventi, attività e diari
+Comment[ja]=イベント、タスク、日記のエントリのための Akonadi シリアライザプラグイン
+Comment[kk]=Akonadi оқиға, тапсырма, күнделік жазуларының тізбектеуіш плагині
+Comment[km]=កម្មវិធី​ជំនួយ​កម្មវិធី​បោះពុម្ព Akonadi សម្រាប់​ធាតុ​​ព្រឹត្តិការណ៍ ភារកិច្ច និង​ទិនានុប្បវត្តិ
+Comment[ko]=행사, 작업, 저널 항목을 위한 Akonadi 시리얼라이저 플러그인
+Comment[lt]=Akonadi serializatoriaus papildinys įvykiams, užduotims ir dienoraščio įrašams
+Comment[lv]=Akonadi notikumu, uzdevumu un dienasgrāmatas ierakstu serializēšanas spraudnis
+Comment[nb]=Et Akonadi programtillegg for serialisering av hendelser, gjøremål og dagboksnotater
+Comment[nds]=Akonadi-Inreegmoduul för Begeefnissen, Opgaven un Daagbookindrääg
+Comment[ne]=घटना, कार्य र जर्नल भौचरका लागि एउटा एकोनाडी मिलानकर्ता प्लगइन
+Comment[nl]=Een administratieplug-in voor Akonadi voor evenementen, taken en journalen
+Comment[nn]=Eit Akonadi-serialisatortillegg for hendingar, oppgåver og dagboktekstar
+Comment[pa]=ਈਵੈਂਟ, ਟਾਸਕ ਅਤੇ ਜਰਨਲ ਐਂਟਰੀਆਂ ਲਈ ਅਕੌਂਡੀ ਸੀਰੀਲਾਈਜ਼ਰ ਪਲੱਗਇਨ
+Comment[pl]=Wtyczka Akonadi do szeregowania zdarzeń, zadań i wpisów w dzienniku
+Comment[pt]=Um 'plugin' de serialização do Akonadi para os eventos, tarefas e itens do diário
+Comment[pt_BR]=Um plugin de serialização do Akonadi para os eventos, tarefas e entradas do diário
+Comment[ro]=Modul de serializare Akonadi pentru evenimente, sarcini și întregistrări de agendă
+Comment[ru]=Модуль сохранения событий, задач и записей дневника для Akonadi
+Comment[sk]=Plugin serializátora Akonadi pre udalosti, úlohy a položky denníka
+Comment[sl]=Akonadijev vstavek za razvrščanje predmetov pojavitev v zaporedje
+Comment[sr]=Аконадијев прикључак серијализатора за догађаје, послове и уносе дневника
+Comment[sr@ijekavian]=Аконадијев прикључак серијализатора за догађаје, послове и уносе дневника
+Comment[sr@ijekavianlatin]=Akonadijev priključak serijalizatora za događaje, poslove i unose dnevnika
+Comment[sr@latin]=Akonadijev priključak serijalizatora za događaje, poslove i unose dnevnika
+Comment[sv]=Ett insticksprogram till Akonadi för serialisering av händelser, uppgifter och journalanteckningar
+Comment[tr]=Olay, görev ve günlük nesneleri için bir Akonadi sıralandırıcısı
+Comment[uk]="Додаток серіалізації Akonadi для подій, завдань і записів журналів"
+Comment[x-test]=xxAn Akonadi serializer plugin for events, tasks and journal entriesxx
+Comment[zh_CN]=对事项、任务和日记项进行序列转换的 Akonadi 插件
+Comment[zh_TW]=事件、工作與日誌項目物件的 Akonadi 序列器外掛程式
+
+[Plugin]
+Type=text/calendar,application/x-vnd.akonadi.note,application/x-vnd.kde.notes
+X-Akonadi-Class=default;KCalCore::Incidence*;
+X-KDE-Library=akonadi_serializer_kcalcore
+X-KDE-ClassName=Akonadi::SerializerPluginKCalCore
diff --git a/plugins/akonadi_serializer_kcalcore.h b/plugins/akonadi_serializer_kcalcore.h
new file mode 100644 (file)
index 0000000..8b9bd3e
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+    Copyright (c) 2007 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef AKONADI_SERIALIZER_KCALCORE_H
+#define AKONADI_SERIALIZER_KCALCORE_H
+
+#include <QtCore/QObject>
+
+#include <AkonadiCore/differencesalgorithminterface.h>
+#include <AkonadiCore/itemserializerplugin.h>
+#include <AkonadiCore/gidextractorinterface.h>
+#include <KCalCore/ICalFormat>
+#include <KCalCore/ICalTimeZones>
+
+namespace Akonadi
+{
+
+class SerializerPluginKCalCore : public QObject,
+    public ItemSerializerPlugin,
+    public DifferencesAlgorithmInterface,
+    public GidExtractorInterface
+
+{
+    Q_OBJECT
+    Q_INTERFACES(Akonadi::ItemSerializerPlugin)
+    Q_INTERFACES(Akonadi::DifferencesAlgorithmInterface)
+    Q_INTERFACES(Akonadi::GidExtractorInterface)
+    Q_PLUGIN_METADATA(IID "org.kde.akonadi.SerializerPluginKCalCore")
+public:
+    SerializerPluginKCalCore();
+    bool deserialize(Item &item, const QByteArray &label, QIODevice &data, int version) Q_DECL_OVERRIDE;
+    void serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version) Q_DECL_OVERRIDE;
+
+    void compare(Akonadi::AbstractDifferencesReporter *reporter,
+                 const Akonadi::Item &leftItem,
+                 const Akonadi::Item &rightItem) Q_DECL_OVERRIDE;
+
+    QString extractGid(const Item &item) const Q_DECL_OVERRIDE;
+
+private:
+    KCalCore::ICalFormat mFormat;
+    QSharedPointer<KCalCore::ICalTimeZones> mTimeZones;
+};
+
+}
+
+#endif
diff --git a/plugins/akonadi_serializer_mail.cpp b/plugins/akonadi_serializer_mail.cpp
new file mode 100644 (file)
index 0000000..8204394
--- /dev/null
@@ -0,0 +1,294 @@
+/*
+    Copyright (c) 2007 Till Adam <adam@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "akonadi_serializer_mail.h"
+#include "akonadi_serializer_mail_debug.h"
+
+#include <QtCore/qplugin.h>
+#include <QtCore/QDataStream>
+
+#include <kmime/kmime_message.h>
+
+#include <AkonadiCore/item.h>
+#include <Akonadi/KMime/MessageParts>
+#include <akonadi/private/imapparser_p.h>
+
+using namespace Akonadi;
+using namespace KMime;
+
+QString StringPool::sharedValue(const QString &value)
+{
+    QMutexLocker lock(&m_mutex);
+    QSet<QString>::const_iterator it = m_pool.constFind(value);
+    if (it != m_pool.constEnd()) {
+        return *it;
+    }
+    m_pool.insert(value);
+    return value;
+}
+
+template <typename T>
+static void parseAddrList(const QVarLengthArray<QByteArray, 16> &addrList, T *hdr,
+                          int version, StringPool &pool)
+{
+    hdr->clear();
+    const int count = addrList.count();
+    QVarLengthArray<QByteArray, 16> addr;
+    for (int i = 0; i < count; ++i) {
+        ImapParser::parseParenthesizedList(addrList[ i ], addr);
+        if (addr.count() != 4) {
+            qCWarning(AKONADI_SERIALIZER_MAIL_LOG) << "Error parsing envelope address field: " << addrList[ i ];
+            continue;
+        }
+        KMime::Types::Mailbox addrField;
+        if (version == 0) {
+            addrField.setNameFrom7Bit(addr[0]);
+        } else if (version == 1) {
+            addrField.setName(pool.sharedValue(QString::fromUtf8(addr[0])));
+        }
+        KMime::Types::AddrSpec addrSpec;
+        addrSpec.localPart = pool.sharedValue(QString::fromUtf8(addr[2]));
+        addrSpec.domain = pool.sharedValue(QString::fromUtf8(addr[3]));
+        addrField.setAddress(addrSpec);
+        hdr->addAddress(addrField);
+    }
+}
+
+template<typename T>
+static void parseAddrList(QDataStream &stream, T *hdr,
+                          int version, StringPool &pool)
+{
+    Q_UNUSED(version);
+
+    hdr->clear();
+    int count = 0;
+    stream >> count;
+    for (int i = 0; i < count; ++i) {
+        QString str;
+        KMime::Types::Mailbox mbox;
+        KMime::Types::AddrSpec addrSpec;
+        stream >> str;
+        mbox.setName(pool.sharedValue(str));
+        stream >> str;
+        addrSpec.localPart = pool.sharedValue(str);
+        stream >> str;
+        addrSpec.domain = pool.sharedValue(str);
+        mbox.setAddress(addrSpec);
+
+        hdr->addAddress(mbox);
+    }
+}
+
+bool SerializerPluginMail::deserialize(Item &item, const QByteArray &label, QIODevice &data, int version)
+{
+    if (label != MessagePart::Body && label != MessagePart::Envelope && label != MessagePart::Header) {
+        return false;
+    }
+
+    KMime::Message::Ptr msg;
+    if (!item.hasPayload()) {
+        Message *m = new  Message();
+        msg = KMime::Message::Ptr(m);
+        item.setPayload(msg);
+    } else {
+        msg = item.payload<KMime::Message::Ptr>();
+    }
+
+    if (label == MessagePart::Body) {
+        QByteArray buffer = data.readAll();
+        if (buffer.isEmpty()) {
+            return true;
+        }
+        msg->setContent(buffer);
+        msg->parse();
+    } else if (label == MessagePart::Header) {
+        QByteArray buffer = data.readAll();
+        if (buffer.isEmpty()) {
+            return true;
+        }
+        if (msg->body().isEmpty() && msg->contents().isEmpty()) {
+            msg->setHead(buffer);
+            msg->parse();
+        }
+    } else if (label == MessagePart::Envelope) {
+        if (version <= 1) {
+            QByteArray buffer = data.readAll();
+            if (buffer.isEmpty()) {
+                return true;
+            }
+            QVarLengthArray<QByteArray, 16> env;
+            ImapParser::parseParenthesizedList(buffer, env);
+            if (env.count() < 10) {
+                qCWarning(AKONADI_SERIALIZER_MAIL_LOG) << "Akonadi KMime Deserializer: Got invalid envelope: " << buffer;
+                return false;
+            }
+            Q_ASSERT(env.count() >= 10);
+            // date
+            msg->date()->from7BitString(env[0]);
+            // subject
+            msg->subject()->from7BitString(env[1]);
+            // from
+            QVarLengthArray<QByteArray, 16> addrList;
+            ImapParser::parseParenthesizedList(env[2], addrList);
+            if (!addrList.isEmpty()) {
+                parseAddrList(addrList, msg->from(), version, m_stringPool);
+            }
+            // sender
+            ImapParser::parseParenthesizedList(env[3], addrList);
+            if (!addrList.isEmpty()) {
+                parseAddrList(addrList, msg->sender(), version, m_stringPool);
+            }
+            // reply-to
+            ImapParser::parseParenthesizedList(env[4], addrList);
+            if (!addrList.isEmpty()) {
+                parseAddrList(addrList, msg->replyTo(), version, m_stringPool);
+            }
+            // to
+            ImapParser::parseParenthesizedList(env[5], addrList);
+            if (!addrList.isEmpty()) {
+                parseAddrList(addrList, msg->to(), version, m_stringPool);
+            }
+            // cc
+            ImapParser::parseParenthesizedList(env[6], addrList);
+            if (!addrList.isEmpty()) {
+                parseAddrList(addrList, msg->cc(), version, m_stringPool);
+            }
+            // bcc
+            ImapParser::parseParenthesizedList(env[7], addrList);
+            if (!addrList.isEmpty()) {
+                parseAddrList(addrList, msg->bcc(), version, m_stringPool);
+            }
+            // in-reply-to
+            msg->inReplyTo()->from7BitString(env[8]);
+            // message id
+            msg->messageID()->from7BitString(env[9]);
+            // references
+            if (env.count() > 10) {
+                msg->references()->from7BitString(env[10]);
+            }
+        } else if (version == 2) {
+            QDataStream stream(&data);
+            QDateTime dt;
+            QString str;
+
+            stream >> dt;
+            msg->date()->setDateTime(dt);
+            stream >> str;
+            msg->subject()->fromUnicodeString(str, "UTF-8");
+            stream >> str;
+            msg->inReplyTo()->fromUnicodeString(str, "UTF-8");
+            stream >> str;
+            msg->messageID()->fromUnicodeString(str, "UTF-8");
+            stream >> str;
+            msg->references()->fromUnicodeString(str, "UTF-8");
+
+            parseAddrList(stream, msg->from(), version, m_stringPool);
+            parseAddrList(stream, msg->sender(), version, m_stringPool);
+            parseAddrList(stream, msg->replyTo(), version, m_stringPool);
+            parseAddrList(stream, msg->to(), version, m_stringPool);
+            parseAddrList(stream, msg->cc(), version, m_stringPool);
+            parseAddrList(stream, msg->bcc(), version, m_stringPool);
+
+            if (stream.status() == QDataStream::ReadCorruptData || stream.status() == QDataStream::ReadPastEnd) {
+                qCWarning(AKONADI_SERIALIZER_MAIL_LOG) << "Akonadi KMime Deserializer: Got invalid envelope";
+                return false;
+            }
+        }
+    }
+
+    return true;
+}
+
+template<typename T>
+static void serializeAddrList(QDataStream &stream, T *hdr)
+{
+    KMime::Types::Mailbox::List mb = hdr->mailboxes();
+    stream << mb.size();
+    foreach (const KMime::Types::Mailbox &mbox, mb) {
+        stream << mbox.name()
+               << mbox.addrSpec().localPart
+               << mbox.addrSpec().domain;
+    }
+}
+
+void SerializerPluginMail::serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version)
+{
+    version = 1;
+
+    KMime::Message::Ptr m = item.payload<KMime::Message::Ptr>();
+    if (label == MessagePart::Body) {
+        data.write(m->encodedContent());
+    } else if (label == MessagePart::Envelope) {
+        version = 2;
+        QDataStream stream(&data);
+        stream << m->date()->dateTime()
+               << m->subject()->asUnicodeString()
+               << m->inReplyTo()->asUnicodeString()
+               << m->messageID()->asUnicodeString()
+               << m->references()->asUnicodeString();
+        serializeAddrList(stream, m->from());
+        serializeAddrList(stream, m->sender());
+        serializeAddrList(stream, m->replyTo());
+        serializeAddrList(stream, m->to());
+        serializeAddrList(stream, m->cc());
+        serializeAddrList(stream, m->bcc());
+    } else if (label == MessagePart::Header) {
+        data.write(m->head());
+    }
+}
+
+QSet<QByteArray> SerializerPluginMail::parts(const Item &item) const
+{
+    QSet<QByteArray> set;
+
+    if (!item.hasPayload<KMime::Message::Ptr>()) {
+        return set;
+    }
+
+    KMime::Message::Ptr msg = item.payload<KMime::Message::Ptr>();
+    if (!msg) {
+        return set;
+    }
+
+    // FIXME: we really want "has any header" here, but the kmime api doesn't offer that yet
+    if (msg->hasContent() || msg->hasHeader("Message-ID")) {
+        set << MessagePart::Envelope << MessagePart::Header;
+        if (!msg->body().isEmpty() || !msg->contents().isEmpty()) {
+            set << MessagePart::Body;
+        }
+    }
+    return set;
+}
+
+QString SerializerPluginMail::extractGid(const Item &item) const
+{
+    if (!item.hasPayload<KMime::Message::Ptr>()) {
+        return QString();
+    }
+    const KMime::Message::Ptr msg = item.payload<KMime::Message::Ptr>();
+    KMime::Headers::MessageID *mid = msg->messageID(false);
+    if (mid) {
+        return mid->asUnicodeString();
+    } else if (KMime::Headers::Base *uid = msg->headerByType("X-Akonotes-UID")) {
+        return uid->asUnicodeString();
+    }
+    return QString();
+}
+
+#include "moc_akonadi_serializer_mail.cpp"
diff --git a/plugins/akonadi_serializer_mail.desktop b/plugins/akonadi_serializer_mail.desktop
new file mode 100644 (file)
index 0000000..f5b3701
--- /dev/null
@@ -0,0 +1,101 @@
+[Misc]
+Name=Mail Serializer
+Name[ar]=مسلسل البريد
+Name[bs]=Serializator maila
+Name[ca]=Serialitzador de correu
+Name[ca@valencia]=Serialitzador de correu
+Name[da]=Serieordning af post
+Name[de]=E-Mail-Serialisierung
+Name[el]=Σειριακοποιητής αλληλογραφίας
+Name[en_GB]=Mail Serialiser
+Name[es]=Serializador de correo
+Name[et]=Kirjade jadasti
+Name[fi]=Sähköpostiserialisoija
+Name[fr]=Sérialiseur de courriers électroniques
+Name[ga]=Srathóir Ríomhphoist
+Name[gl]=Serializador de correo
+Name[hu]=Levélkezelő
+Name[ia]=Divulgator partial de posta
+Name[it]=Serializzatore posta elettronica
+Name[ja]=メール用シリアライザ
+Name[kk]=Пошта тізбектеуіші
+Name[km]=ម៉ាស៊ីន​បោះពុម្ព​សំបុត្រ
+Name[ko]=메일 시리얼라이저
+Name[lt]=Pašto serializatorius
+Name[lv]=Pasta serializētājs
+Name[nb]=E-postserialisator
+Name[nds]=Nettpost-Reegmoduul
+Name[ne]=पत्र मलानकर्ता
+Name[nl]=E-mailadministratie
+Name[nn]=E-postserialisator
+Name[pa]=ਮੇਲ ਸੀਰੀਅਲਾਈਜ਼ਰ
+Name[pl]=Szeregowanie poczty
+Name[pt]=Serializador de Correio
+Name[pt_BR]=Serializador de mensagem
+Name[ro]=Serializator de poștă
+Name[ru]=Сохранение писем
+Name[sk]=Serializátor pošty
+Name[sl]=Razvrščevalnik pošte v zaporedje
+Name[sr]=Серијализатор поште
+Name[sr@ijekavian]=Серијализатор поште
+Name[sr@ijekavianlatin]=Serijalizator pošte
+Name[sr@latin]=Serijalizator pošte
+Name[sv]=E-postserialisering
+Name[tr]=E-posta Sıralandırıcı
+Name[uk]=Серіалізатор пошти
+Name[x-test]=xxMail Serializerxx
+Name[zh_CN]=邮件序列转换器
+Name[zh_TW]=信件序列器
+Comment=An Akonadi serializer plugin for mail objects
+Comment[ar]=ملحق مسلسل اكوندا لكائنات البريد
+Comment[bs]=Akonadi dodatak serializatora za mail objekte
+Comment[ca]=Un connector de serialització de l'Akonadi pels objectes correu
+Comment[ca@valencia]=Un connector de serialització de l'Akonadi pels objectes correu
+Comment[da]=Et Akonadi-plugin til serieordning af postobjekter
+Comment[de]=Akonadi-Modul zur Serialisierung von E-Mail-Objekten
+Comment[el]=Ένα πρόσθετο σειριακοποιητή Akonadi για αντικείμενα αλληλογραφίας
+Comment[en_GB]=An Akonadi serialiser plugin for mail objects
+Comment[es]=Un complemento serializador de Akonadi para objetos correo
+Comment[et]=Akonadi kirjaobjektide jadastamisplugin
+Comment[fi]=Akonadi-serialisoijaliitännäinen sähköpostiobjekteille
+Comment[fr]=Un module externe Akonadi pour la sérialisation des courriers électroniques
+Comment[ga]=Breiseán srathóra Akonadi le haghaidh ríomhphoist
+Comment[gl]=Un engadido de serialización do Akonadi para obxectos correo
+Comment[hu]=Akonadi-modul levelek kezeléséhez
+Comment[ia]=Un plug-in pro divulgator partial de Akonadi pro objectos de posta
+Comment[it]=Un'estensione di Akonadi per la serializzazione di posta elettronica
+Comment[ja]=メールオブジェクトのための Akonadi シリアライザプラグイン
+Comment[kk]=Akonadi пошта нысандарының тізбектеуіш плагині
+Comment[km]=កម្មវិធី​ជំនួយ​កម្មវិធី​បោះពុម្ព Akonadi សម្រាប់​វត្ថុ​សំបុត្រ
+Comment[ko]=메일 객체를 위한 Akonadi 시리얼라이저 플러그인
+Comment[lt]=Akonadi serializatoriaus papildinys pašto objektams
+Comment[lv]=Akonadi pasta serializēšanas spraudnis
+Comment[nb]=Et Akonadi programtillegg for serialisering av e-postobjekter
+Comment[nds]=Akonadi-Inreegmoduul för Nettbreven
+Comment[ne]=पत्र वस्तुका लागि एउटा एकोनाडी मिलानकर्ता प्लगइन
+Comment[nl]=Een administratieplug-in voor Akonadi voor e-mails
+Comment[nn]=Eit Akonadi-serialisatortillegg for e-postobjekt
+Comment[pa]=ਮੇਲ ਆਬਜੈਕਟ ਲਈ ਅਕੌਂਡੀ ਸੀਰੀਲਾਈਜ਼ਰ
+Comment[pl]=Wtyczka Akonadi do szeregowania obiektów pocztowych
+Comment[pt]=Um 'plugin' de serialização do Akonadi para os objectos de correio
+Comment[pt_BR]=Um plugin de serialização do Akonadi para os objetos de mensagem
+Comment[ro]=Modul de serializare Akonadi pentru obiecte poștă
+Comment[ru]=Модуль сохранения писем для Akonadi
+Comment[sk]=Plugin serializátora Akonadi pre poštové objekty
+Comment[sl]=Akonadijev vstavek za razvrščanje predmetov pošte v zaporedje
+Comment[sr]=Аконадијев прикључак серијализатора за објекте поште
+Comment[sr@ijekavian]=Аконадијев прикључак серијализатора за објекте поште
+Comment[sr@ijekavianlatin]=Akonadijev priključak serijalizatora za objekte pošte
+Comment[sr@latin]=Akonadijev priključak serijalizatora za objekte pošte
+Comment[sv]=Ett insticksprogram till Akonadi för serialisering av e-postobjekt
+Comment[tr]=E-posta nesneleri için bir Akonadi sıralandırıcısı
+Comment[uk]=Додаток серіалізації Akonadi для об'єктів пошти
+Comment[x-test]=xxAn Akonadi serializer plugin for mail objectsxx
+Comment[zh_CN]=对邮件对象进行序列转换的 Akonadi 插件
+Comment[zh_TW]=信件物件的 Akonadi 序列器外掛程式
+
+[Plugin]
+Type=message/rfc822,message/news,text/x-vnd.akonadi.note
+X-Akonadi-Class=legacy;default;KMime::Message*;
+X-KDE-Library=akonadi_serializer_mail
+X-KDE-ClassName=Akonadi::SerializerPluginMail
diff --git a/plugins/akonadi_serializer_mail.h b/plugins/akonadi_serializer_mail.h
new file mode 100644 (file)
index 0000000..bfac224
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+    Copyright (c) 2007 Till Adam <adam@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef __AKONADI_SERIALIZER_MAIL_H__
+#define __AKONADI_SERIALIZER_MAIL_H__
+
+#include <QtCore/QObject>
+#include <QtCore/QMutex>
+
+#include <AkonadiCore/itemserializerplugin.h>
+#include <AkonadiCore/gidextractorinterface.h>
+
+namespace Akonadi
+{
+
+/**
+ * Levare QString implicit sharing to decrease memory consumption.
+ *
+ * This class is thread safe. Apparenlty required for usage in
+ * legacy KRes compat bridges.
+ */
+class StringPool
+{
+public:
+    /**
+     * Lookup @p value in the pool and return the known value
+     * to reuse it and leverage the implicit sharing. Otherwise
+     * add the value to the pool and return it again.
+     */
+    QString sharedValue(const QString &value);
+private:
+    QMutex m_mutex;
+    QSet<QString> m_pool;
+};
+
+class SerializerPluginMail : public QObject, public ItemSerializerPlugin, public GidExtractorInterface
+{
+    Q_OBJECT
+    Q_INTERFACES(Akonadi::ItemSerializerPlugin Akonadi::GidExtractorInterface)
+    Q_PLUGIN_METADATA(IID "org.kde.akonadi.SerializerPluginMail")
+public:
+    bool deserialize(Item &item, const QByteArray &label, QIODevice &data, int version) Q_DECL_OVERRIDE;
+    void serialize(const Item &item, const QByteArray &label, QIODevice &data, int &version) Q_DECL_OVERRIDE;
+    QSet<QByteArray> parts(const Item &item) const Q_DECL_OVERRIDE;
+    QString extractGid(const Item &item) const Q_DECL_OVERRIDE;
+private:
+    StringPool m_stringPool;
+};
+
+}
+
+#endif
diff --git a/plugins/autotests/CMakeLists.txt b/plugins/autotests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..4beeb08
--- /dev/null
@@ -0,0 +1,32 @@
+include(ECMAddTests)
+
+find_package(Qt5Test CONFIG REQUIRED)
+
+set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} )
+
+
+include_directories(
+    ${kdepim-runtime_SOURCE_DIR}/plugins
+    ${kdepim-runtime_BINARY_DIR}/plugins
+)
+
+# convenience macro to add akonadi qtestlib unit-tests
+macro(add_akonadiplugin_test _source _libs _additionalSources)
+  set(_test ${_source})
+  set(srcs ${_test} ${_additionalSources})
+
+  get_filename_component(_name ${_source} NAME_WE)
+  add_executable( ${_name} ${srcs} )
+  add_test( ${_name} ${_name} )
+  ecm_mark_as_test(akonadiplugin-${_name})
+
+  target_link_libraries(${_name} KF5::AkonadiCore KF5::AkonadiMime 
+                         Qt5::Test KF5::AkonadiPrivate KF5::I18n
+                        KF5::AkonadiPrivate ${_libs})
+endmacro()
+
+# qtestlib unit tests
+add_akonadiplugin_test(mailserializertest.cpp "KF5::Mime" "../akonadi_serializer_mail_debug.cpp")
+add_akonadiplugin_test(mailserializerplugintest.cpp "KF5::Mime" "")
+add_akonadiplugin_test(kcalcoreserializertest.cpp "KF5::CalendarCore" "")
+add_akonadiplugin_test(addresseeserializertest.cpp "KF5::Contacts;KF5::AkonadiContact" "../akonadi_serializer_addressee.cpp")
diff --git a/plugins/autotests/addresseeserializertest.cpp b/plugins/autotests/addresseeserializertest.cpp
new file mode 100644 (file)
index 0000000..b6ece41
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+    Copyright (c) 2013 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include <AkonadiCore/item.h>
+#include <qtest.h>
+#include <QtCore/QObject>
+#include <akonadi_serializer_addressee.h>
+
+using namespace Akonadi;
+
+class AddresseeSerializerTest : public QObject
+{
+    Q_OBJECT
+private Q_SLOTS:
+    void testGid()
+    {
+        const QString uid(QStringLiteral("uid"));
+        KContacts::Addressee addressee;
+        addressee.setUid(uid);
+        Akonadi::Item item;
+        item.setMimeType(addressee.mimeType());
+        item.setPayload(addressee);
+        SerializerPluginAddressee plugin;
+        const QString gid = plugin.extractGid(item);
+        QCOMPARE(gid, uid);
+    }
+};
+
+QTEST_MAIN(AddresseeSerializerTest)
+
+#include "addresseeserializertest.moc"
diff --git a/plugins/autotests/kcalcoreserializertest.cpp b/plugins/autotests/kcalcoreserializertest.cpp
new file mode 100644 (file)
index 0000000..77ca212
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+    Copyright (c) 2009 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include <AkonadiCore/item.h>
+#include <KCalCore/Event>
+#include <qtest.h>
+#include <QtCore/QObject>
+
+using namespace Akonadi;
+using namespace KCalCore;
+
+class KCalCoreSerializerTest : public QObject
+{
+    Q_OBJECT
+private Q_SLOTS:
+    void testEventSerialize_data()
+    {
+        QTest::addColumn<QString>("mimeType");
+        QTest::newRow("specific") << "application/x-vnd.akonadi.calendar.event";
+        QTest::newRow("generic") << "text/calendar";
+    }
+    void testCharsets_data()
+    {
+        testEventSerialize_data();
+    }
+
+    void testEventSerialize()
+    {
+        QFETCH(QString, mimeType);
+
+        QByteArray serialized =
+            "BEGIN:VCALENDAR\n"
+            "PRODID:-//K Desktop Environment//NONSGML libkcal 3.5//EN\n"
+            "VERSION:2.0\n"
+            "BEGIN:VEVENT\n"
+            "DTSTAMP:20070109T100625Z\n"
+            "ORGANIZER;CN=\"Volker Krause\":MAILTO:vkrause@kde.org\n"
+            "CREATED:20070109T100553Z\n"
+            "UID:libkcal-1135684253.945\n"
+            "SEQUENCE:1\n"
+            "LAST-MODIFIED:20070109T100625Z\n"
+            "SUMMARY:Test event\n"
+            "LOCATION:here\n"
+            "CLASS:PUBLIC\n"
+            "PRIORITY:5\n"
+            "CATEGORIES:KDE\n"
+            "DTSTART:20070109T183000Z\n"
+            "DTEND:20070109T225900Z\n"
+            "TRANSP:OPAQUE\n"
+            "BEGIN:VALARM\n"
+            "DESCRIPTION:\n"
+            "ACTION:DISPLAY\n"
+            "TRIGGER;VALUE=DURATION:-PT45M\n"
+            "END:VALARM\n"
+            "END:VEVENT\n"
+            "END:VCALENDAR\n";
+
+        // deserializing
+        Item item;
+        item.setMimeType(mimeType);
+        item.setPayloadFromData(serialized);
+
+        QVERIFY(item.hasPayload<Event::Ptr>());
+        const Event::Ptr event = item.payload<Event::Ptr>();
+        QVERIFY(event != 0);
+
+        QCOMPARE(event->summary(), QStringLiteral("Test event"));
+        QCOMPARE(event->location(), QStringLiteral("here"));
+
+        // serializing
+        const QByteArray data = item.payloadData();
+        QVERIFY(!data.isEmpty());
+    }
+
+    void testCharsets()
+    {
+        QFETCH(QString, mimeType);
+
+        // 0 defaults to latin1.
+        //QT5 QVERIFY( QTextCodec::codecForCStrings() == 0 );
+
+        const QDate currentDate = QDate::currentDate();
+
+        Event::Ptr event = Event::Ptr(new Event());
+        event->setUid(QStringLiteral("12345"));
+        event->setDtStart(KDateTime(currentDate));
+        event->setDtEnd(KDateTime(currentDate.addDays(1)));
+
+        // ü
+        const char latin1_umlaut[] = { static_cast<char>(0xFC), '\0' };
+        event->setSummary(QLatin1String(latin1_umlaut));
+
+        Item item;
+        item.setMimeType(mimeType);
+        item.setPayload(event);
+
+        // Serializer the item, the serialization should be in UTF-8:
+        const char utf_umlaut[] = { static_cast<char>(0xC3), static_cast<char>(0XBC), '\0' };
+        const QByteArray bytes = item.payloadData();
+        QVERIFY(bytes.contains(utf_umlaut));
+        QVERIFY(!bytes.contains(latin1_umlaut));
+
+        // Deserialize the data:
+        Item item2;
+        item2.setMimeType(mimeType);
+        item2.setPayloadFromData(bytes);
+
+        Event::Ptr event2 = item2.payload<Event::Ptr>();
+        QVERIFY(event2 != 0);
+        QVERIFY(event2->summary().toUtf8() == QByteArray(utf_umlaut));
+        QVERIFY(event2->summary().toLatin1() == QByteArray(latin1_umlaut));
+    }
+};
+
+QTEST_MAIN(KCalCoreSerializerTest)
+
+#include "kcalcoreserializertest.moc"
diff --git a/plugins/autotests/mailserializerplugintest.cpp b/plugins/autotests/mailserializerplugintest.cpp
new file mode 100644 (file)
index 0000000..04103a3
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+    Copyright (c) 2007 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "mailserializerplugintest.h"
+
+#include <AkonadiCore/item.h>
+#include <kmime/kmime_message.h>
+#include <QDebug>
+#include <qtest.h>
+
+QTEST_MAIN(MailSerializerPluginTest)
+
+using namespace Akonadi;
+using namespace KMime;
+
+void MailSerializerPluginTest::testMailPlugin()
+{
+    QByteArray serialized =
+        "From: sender@test.org\n"
+        "Subject: Serializer Test\n"
+        "To: receiver@test.org\n"
+        "Date: Fri, 22 Jun 2007 17:24:24 +0000\n"
+        "MIME-Version: 1.0\n"
+        "Content-Type: text/plain\n"
+        "\n"
+        "Body data.";
+
+    // deserializing
+    Item item;
+    item.setMimeType(QStringLiteral("message/rfc822"));
+    item.setPayloadFromData(serialized);
+
+    QVERIFY(item.hasPayload<KMime::Message::Ptr>());
+    KMime::Message::Ptr msg = item.payload<KMime::Message::Ptr>();
+    QVERIFY(msg != 0);
+
+    QCOMPARE(msg->to()->asUnicodeString(), QStringLiteral("receiver@test.org"));
+    QCOMPARE(msg->body(), QByteArray("Body data."));
+
+    // serializing
+    QByteArray data = item.payloadData();
+    QCOMPARE(data, serialized);
+}
+
+void MailSerializerPluginTest::testMessageIntegrity()
+{
+    // A message that will be slightly modified if KMime::Content::assemble() is
+    // called.  We want to avoid this, because it breaks signatures.
+    QByteArray serialized =
+        "from: sender@example.com\n"
+        "to: receiver@example.com\n"
+        "Subject: Serializer Test\n"
+        "Date: Thu, 30 Jul 2009 13:46:31 +0300\n"
+        "MIME-Version: 1.0\n"
+        "Content-type: text/plain; charset=us-ascii\n"
+        "\n"
+        "Bla bla bla.";
+
+    // Deserialize.
+    Item item;
+    item.setMimeType(QStringLiteral("message/rfc822"));
+    item.setPayloadFromData(serialized);
+
+    QVERIFY(item.hasPayload<KMime::Message::Ptr>());
+    KMime::Message::Ptr msg = item.payload<KMime::Message::Ptr>();
+    QVERIFY(msg != 0);
+
+    qDebug() << "original data:" << serialized;
+    qDebug() << "message content:" << msg->encodedContent();
+    QCOMPARE(msg->encodedContent(), serialized);
+
+    // Serialize.
+    QByteArray data = item.payloadData();
+    qDebug() << "original data:" << serialized;
+    qDebug() << "serialized data:" << data;
+    QCOMPARE(data, serialized);
+}
+
diff --git a/plugins/autotests/mailserializerplugintest.h b/plugins/autotests/mailserializerplugintest.h
new file mode 100644 (file)
index 0000000..9324735
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+    Copyright (c) 2007 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef MAILSERIALIZERPLUGINTEST_H
+#define MAILSERIALIZERPLUGINTEST_H
+
+#include <QtCore/QObject>
+
+class MailSerializerPluginTest : public QObject
+{
+    Q_OBJECT
+private Q_SLOTS:
+    void testMailPlugin();
+    void testMessageIntegrity();
+};
+
+#endif
diff --git a/plugins/autotests/mailserializertest.cpp b/plugins/autotests/mailserializertest.cpp
new file mode 100644 (file)
index 0000000..b24d59c
--- /dev/null
@@ -0,0 +1,372 @@
+/*
+    Copyright (c) 2007 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "mailserializertest.h"
+
+#include "akonadi_serializer_mail.cpp"
+
+#include <Akonadi/KMime/MessageParts>
+
+#include <qtest.h>
+#include <QBuffer>
+
+QTEST_MAIN(MailSerializerTest)
+
+void MailSerializerTest::testEnvelopeDeserialize_data()
+{
+    QTest::addColumn<int>("version");
+    QTest::addColumn<QByteArray>("data");
+    QTest::addColumn<QDateTime>("date");
+    QTest::addColumn<QString>("subject");
+    QTest::addColumn<QString>("from");
+    QTest::addColumn<QString>("sender");
+    QTest::addColumn<QString>("replyTo");
+    QTest::addColumn<QString>("to");
+    QTest::addColumn<QString>("cc");
+    QTest::addColumn<QString>("bcc");
+    QTest::addColumn<QString>("inReplyTo");
+    QTest::addColumn<QString>("messageId");
+    QTest::addColumn<QString>("references");
+
+    QTest::newRow("v1 - no references")
+            << 1
+            << QByteArray("(\"Wed, 1 Feb 2006 13:37:19 UT\" \"IMPORTANT: Akonadi Test\" ((\"Tobias Koenig\" NIL \"tokoe\" \"kde.org\")) ((\"Tobias Koenig\" NIL \"tokoe\" \"kde.org\")) NIL ((\"Ingo Kloecker\" NIL \"kloecker\" \"kde.org\")) NIL NIL NIL <{7b55527e-77f4-489d-bf18-e805be96718c}@server.kde.org>)")
+            << QDateTime(QDate(2006, 2, 1), QTime(13, 37, 19), Qt::UTC)
+            << QString::fromUtf8("IMPORTANT: Akonadi Test")
+            << QString::fromUtf8("Tobias Koenig <tokoe@kde.org>")
+            << QString::fromUtf8("Tobias Koenig <tokoe@kde.org>")
+            << QString()
+            << QString::fromUtf8("Ingo Kloecker <kloecker@kde.org>")
+            << QString() << QString() << QString()
+            << QString::fromUtf8("<{7b55527e-77f4-489d-bf18-e805be96718c}@server.kde.org>")
+            << QString();
+
+    QTest::newRow("v1 - with references")
+            << 1
+            << QByteArray("(\"Wed, 1 Feb 2006 13:37:19 UT\" \"IMPORTANT: Akonadi Test\" ((\"Tobias Koenig\" NIL \"tokoe\" \"kde.org\")) ((\"Tobias Koenig\" NIL \"tokoe\" \"kde.org\")) NIL ((\"Ingo Kloecker\" NIL \"kloecker\" \"kde.org\")) NIL NIL NIL <{7b55527e-77f4-489d-bf18-e805be96718c}@server.kde.org> \"<{8888827e-77f4-489d-bf18-e805be96718c}@server.kde.org> <{9999927e-77f4-489d-bf18-e805be96718c}@server.kde.org>\")")
+            << QDateTime(QDate(2006, 2, 1), QTime(13, 37, 19), Qt::UTC)
+            << QString::fromUtf8("IMPORTANT: Akonadi Test")
+            << QString::fromUtf8("Tobias Koenig <tokoe@kde.org>")
+            << QString::fromUtf8("Tobias Koenig <tokoe@kde.org>")
+            << QString()
+            << QString::fromUtf8("Ingo Kloecker <kloecker@kde.org>")
+            << QString()
+            << QString()
+            << QString()
+            << QString::fromUtf8("<{7b55527e-77f4-489d-bf18-e805be96718c}@server.kde.org>")
+            << QString::fromUtf8("<{8888827e-77f4-489d-bf18-e805be96718c}@server.kde.org> <{9999927e-77f4-489d-bf18-e805be96718c}@server.kde.org>");
+
+    QBuffer buffer;
+    buffer.open(QIODevice::WriteOnly);
+    QDataStream stream(&buffer);
+    stream << QDateTime(QDate(2015, 6, 29), QTime(23, 50, 10), Qt::UTC)
+           << QString::fromUtf8("Důležité: Testování Akonadi")
+           << QString::fromUtf8("<1234567.icameup@withthis>")
+           << QString::fromUtf8("<2849719.IhmDc3qecs@thor>")
+           << QString::fromUtf8("<1234567.icameup@withthis> <7654321.icameupwith@thistoo>")
+           << 1 << QString::fromUtf8("Daniel Vrátil") << QString::fromUtf8("dvratil") << QString::fromUtf8("kde.org")
+           << 1 << QString::fromUtf8("Daniel Vrátil") << QString::fromUtf8("dvratil") << QString::fromUtf8("kde.org")
+           << 0
+           << 1 << QString::fromUtf8("Volker Krause") << QString::fromUtf8("vkrause") << QString::fromUtf8("kde.org")
+           << 0
+           << 0;
+
+    QTest::newRow("v2")
+            << 2
+            << buffer.data()
+            << QDateTime(QDate(2015, 6, 29), QTime(23, 50, 10), Qt::UTC)
+            << QString::fromUtf8("Důležité: Testování Akonadi")
+            << QString::fromUtf8("Daniel Vrátil <dvratil@kde.org>")
+            << QString::fromUtf8("Daniel Vrátil <dvratil@kde.org>")
+            << QString() // reply to
+            << QString::fromUtf8("Volker Krause <vkrause@kde.org>")
+            << QString() // cc
+            << QString() // bcc
+            << QString::fromUtf8("<1234567.icameup@withthis>")
+            << QString::fromUtf8("<2849719.IhmDc3qecs@thor>")
+            << QString::fromUtf8("<1234567.icameup@withthis> <7654321.icameupwith@thistoo>");
+}
+
+void MailSerializerTest::testEnvelopeDeserialize()
+{
+    QFETCH(int, version);
+    QFETCH(QByteArray, data);
+    QFETCH(QDateTime, date);
+    QFETCH(QString, subject);
+    QFETCH(QString, from);
+    QFETCH(QString, sender);
+    QFETCH(QString, replyTo);
+    QFETCH(QString, to);
+    QFETCH(QString, cc);
+    QFETCH(QString, bcc);
+    QFETCH(QString, inReplyTo);
+    QFETCH(QString, messageId);
+    QFETCH(QString, references);
+
+    Item i;
+    i.setMimeType(QStringLiteral("message/rfc822"));
+
+    SerializerPluginMail *serializer = new SerializerPluginMail();
+
+    // envelope
+    QBuffer buffer;
+    buffer.setData(data);
+    buffer.open(QIODevice::ReadOnly);
+    QBENCHMARK {
+        buffer.seek(0);
+        serializer->deserialize(i, MessagePart::Envelope, buffer, version);
+    }
+    QVERIFY(i.hasPayload<KMime::Message::Ptr>());
+
+    KMime::Message::Ptr msg = i.payload<KMime::Message::Ptr>();
+    QCOMPARE(msg->date()->dateTime(), date);
+    QCOMPARE(msg->subject()->asUnicodeString(), subject);
+    QCOMPARE(msg->from()->asUnicodeString(), from);
+    QCOMPARE(msg->sender()->asUnicodeString(), sender);
+    QCOMPARE(msg->replyTo()->asUnicodeString(), replyTo);
+    QCOMPARE(msg->to()->asUnicodeString(), to);
+    QCOMPARE(msg->cc()->asUnicodeString(), cc);
+    QCOMPARE(msg->bcc()->asUnicodeString(), bcc);
+    QCOMPARE(msg->inReplyTo()->asUnicodeString(), inReplyTo);
+    QCOMPARE(msg->messageID()->asUnicodeString(), messageId);
+    QCOMPARE(msg->references()->asUnicodeString(), references);
+
+    delete serializer;
+}
+
+void MailSerializerTest::testEnvelopeSerialize_data()
+{
+    QTest::addColumn<QDateTime>("date");
+    QTest::addColumn<QString>("subject");
+    QTest::addColumn<QString>("from");
+    QTest::addColumn<QString>("sender");
+    QTest::addColumn<QString>("replyTo");
+    QTest::addColumn<QString>("to");
+    QTest::addColumn<QString>("cc");
+    QTest::addColumn<QString>("bcc");
+    QTest::addColumn<QString>("inReplyTo");
+    QTest::addColumn<QString>("messageId");
+    QTest::addColumn<QString>("references");
+    QTest::addColumn<QByteArray>("data");
+
+    QBuffer buffer;
+    buffer.open(QIODevice::WriteOnly);
+    QDataStream stream(&buffer);
+    stream << QDateTime(QDate(2006, 2, 1), QTime(13, 37, 19), Qt::UTC)
+           << QString::fromUtf8("IMPORTANT: Akonadi Test")
+           << QString::fromUtf8("")
+           << QString::fromUtf8("<{7b55527e-77f4-489d-bf18-e805be96718c}@server.kde.org>")
+           << QString::fromUtf8("")
+           << 1 << QString::fromUtf8("Tobias Koenig") << QString::fromUtf8("tokoe") << QString::fromUtf8("kde.org")
+           << 1 << QString::fromUtf8("Tobias Koenig") << QString::fromUtf8("tokoe") << QString::fromUtf8("kde.org")
+           << 0
+           << 1 << QString::fromUtf8("Ingo Kloecker") << QString::fromUtf8("kloecker") << QString::fromUtf8("kde.org")
+           << 0
+           << 0;
+    QTest::newRow("")
+            << QDateTime(QDate(2006, 2, 1), QTime(13, 37, 19), Qt::UTC)
+            << QString::fromUtf8("IMPORTANT: Akonadi Test")
+            << QString::fromUtf8("Tobias Koenig <tokoe@kde.org>")
+            << QString::fromUtf8("Tobias Koenig <tokoe@kde.org>")
+            << QString()
+            << QString::fromUtf8("Ingo Kloecker <kloecker@kde.org>")
+            << QString()
+            << QString()
+            << QString()
+            << QString::fromUtf8("<{7b55527e-77f4-489d-bf18-e805be96718c}@server.kde.org>")
+            << QString()
+            << buffer.data();
+}
+
+void MailSerializerTest::testEnvelopeSerialize()
+{
+    QFETCH(QDateTime, date);
+    QFETCH(QString, subject);
+    QFETCH(QString, from);
+    QFETCH(QString, sender);
+    QFETCH(QString, replyTo);
+    QFETCH(QString, to);
+    QFETCH(QString, cc);
+    QFETCH(QString, bcc);
+    QFETCH(QString, inReplyTo);
+    QFETCH(QString, messageId);
+    QFETCH(QString, references);
+    QFETCH(QByteArray, data);
+
+    Item i;
+    i.setMimeType(QStringLiteral("message/rfc822"));
+    Message *msg = new Message();
+    msg->date()->setDateTime(date);
+    msg->subject()->fromUnicodeString(subject, "UTF-8");
+    msg->from()->fromUnicodeString(from, "UTF-8");
+    msg->sender()->fromUnicodeString(sender, "UTF-8");
+    msg->replyTo()->fromUnicodeString(replyTo, "UTF-8");
+    msg->to()->fromUnicodeString(to, "UTF-8");
+    msg->cc()->fromUnicodeString(cc, "UTF-8");
+    msg->bcc()->fromUnicodeString(bcc, "UTF-8");
+    msg->inReplyTo()->fromUnicodeString(inReplyTo, "UTF-8");
+    msg->messageID()->fromUnicodeString(messageId, "UTF-8");
+    msg->references()->fromUnicodeString(references, "UTF-8");
+    i.setPayload(KMime::Message::Ptr(msg));
+
+    SerializerPluginMail *serializer = new SerializerPluginMail();
+
+    // envelope
+    QByteArray env;
+    QBuffer buffer;
+    buffer.setBuffer(&env);
+    buffer.open(QIODevice::ReadWrite);
+    int version = 0;
+    QBENCHMARK {
+        buffer.seek(0);
+        serializer->serialize(i, MessagePart::Envelope, buffer, version);
+    }
+    QCOMPARE(env, data);
+
+    delete serializer;
+}
+
+void MailSerializerTest::testParts()
+{
+    Item item;
+    item.setMimeType(QStringLiteral("message/rfc822"));
+    KMime::Message *m = new Message;
+    KMime::Message::Ptr msg(m);
+    item.setPayload(msg);
+
+    SerializerPluginMail *serializer = new SerializerPluginMail();
+    QVERIFY(serializer->parts(item).isEmpty());
+
+    msg->setHead("foo");
+    QSet<QByteArray> parts = serializer->parts(item);
+    QCOMPARE(parts.count(), 2);
+    QVERIFY(parts.contains(MessagePart::Envelope));
+    QVERIFY(parts.contains(MessagePart::Header));
+
+    msg->setBody("bar");
+    parts = serializer->parts(item);
+    QCOMPARE(parts.count(), 3);
+    QVERIFY(parts.contains(MessagePart::Envelope));
+    QVERIFY(parts.contains(MessagePart::Header));
+    QVERIFY(parts.contains(MessagePart::Body));
+
+    delete serializer;
+}
+
+void MailSerializerTest::testHeaderFetch()
+{
+    Item i;
+    i.setMimeType(QStringLiteral("message/rfc822"));
+
+    SerializerPluginMail *serializer = new SerializerPluginMail();
+
+    QByteArray headerData("From: David Johnson <david@usermode.org>\n"
+                          "To: kde-commits@kde.org\n"
+                          "MIME-Version: 1.0\n"
+                          "Date: Sun, 01 Feb 2009 06:25:22 +0000\n"
+                          "Message-Id: <1233469522.741324.18468.nullmailer@svn.kde.org>\n"
+                          "Subject: [kde-doc-english] KDE/kdeutils/kcalc\n");
+
+    QString expectedSubject = QString::fromUtf8("[kde-doc-english] KDE/kdeutils/kcalc");
+    QString expectedFrom = QString::fromUtf8("David Johnson <david@usermode.org>");
+    QString expectedTo = QString::fromUtf8("kde-commits@kde.org");
+
+    // envelope
+    QBuffer buffer;
+    buffer.setData(headerData);
+    buffer.open(QIODevice::ReadOnly);
+    buffer.seek(0);
+    serializer->deserialize(i, MessagePart::Header, buffer, 0);
+    QVERIFY(i.hasPayload<KMime::Message::Ptr>());
+
+    KMime::Message::Ptr msg = i.payload<KMime::Message::Ptr>();
+    QCOMPARE(msg->subject()->asUnicodeString(), expectedSubject);
+    QCOMPARE(msg->from()->asUnicodeString(), expectedFrom);
+    QCOMPARE(msg->to()->asUnicodeString(), expectedTo);
+
+    delete serializer;
+}
+
+void MailSerializerTest::testMultiDeserialize()
+{
+    // The Body part includes the Header.
+    // When serialization is done a second time, we should already have the header deserialized.
+    // We change the header data for the second deserialization (which is an unrealistic scenario)
+    // to demonstrate that it is not deserialized again.
+
+    Item i;
+    i.setMimeType(QStringLiteral("message/rfc822"));
+
+    SerializerPluginMail *serializer = new SerializerPluginMail();
+
+    QByteArray messageData("From: David Johnson <david@usermode.org>\n"
+                           "To: kde-commits@kde.org\n"
+                           "MIME-Version: 1.0\n"
+                           "Date: Sun, 01 Feb 2009 06:25:22 +0000\n"
+                           "Subject: [kde-doc-english] KDE/kdeutils/kcalc\n"
+                           "Content-Type: text/plain\n"
+                           "\n"
+                           "This is content");
+
+    QString expectedSubject = QString::fromUtf8("[kde-doc-english] KDE/kdeutils/kcalc");
+    QString expectedFrom = QString::fromUtf8("David Johnson <david@usermode.org>");
+    QString expectedTo = QString::fromUtf8("kde-commits@kde.org");
+    QByteArray expectedBody("This is content");
+
+    // envelope
+    QBuffer buffer;
+    buffer.setData(messageData);
+    buffer.open(QIODevice::ReadOnly);
+    buffer.seek(0);
+    serializer->deserialize(i, MessagePart::Body, buffer, 0);
+    QVERIFY(i.hasPayload<KMime::Message::Ptr>());
+
+    KMime::Message::Ptr msg = i.payload<KMime::Message::Ptr>();
+    QCOMPARE(msg->subject()->asUnicodeString(), expectedSubject);
+    QCOMPARE(msg->from()->asUnicodeString(), expectedFrom);
+    QCOMPARE(msg->to()->asUnicodeString(), expectedTo);
+    QCOMPARE(msg->body(), expectedBody);
+
+    buffer.close();
+
+    messageData = QByteArray("From: DIFFERENT CONTACT <DIFFERENTCONTACT@example.org>\n"
+                             "To: kde-commits@kde.org\n"
+                             "MIME-Version: 1.0\n"
+                             "Date: Sun, 01 Feb 2009 06:25:22 +0000\n"
+                             "Message-Id: <1233469522.741324.18468.nullmailer@svn.kde.org>\n"
+                             "Subject: [kde-doc-english] KDE/kdeutils/kcalc\n"
+                             "Content-Type: text/plain\n"
+                             "\r\n"
+                             "This is content");
+
+    buffer.setData(messageData);
+    buffer.open(QIODevice::ReadOnly);
+    buffer.seek(0);
+
+    serializer->deserialize(i, MessagePart::Header, buffer, 0);
+    QVERIFY(i.hasPayload<KMime::Message::Ptr>());
+
+    msg = i.payload<KMime::Message::Ptr>();
+    QCOMPARE(msg->subject()->asUnicodeString(), expectedSubject);
+    QCOMPARE(msg->from()->asUnicodeString(), expectedFrom);
+    QCOMPARE(msg->to()->asUnicodeString(), expectedTo);
+
+    delete serializer;
+}
+
diff --git a/plugins/autotests/mailserializertest.h b/plugins/autotests/mailserializertest.h
new file mode 100644 (file)
index 0000000..9f7787e
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+    Copyright (c) 2007 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef MAILSERIALIZERTEST_H
+#define MAILSERIALIZERTEST_H
+
+#include <QtCore/QObject>
+
+class MailSerializerTest : public QObject
+{
+    Q_OBJECT
+private Q_SLOTS:
+    void testEnvelopeDeserialize_data();
+    void testEnvelopeDeserialize();
+
+    void testEnvelopeSerialize_data();
+    void testEnvelopeSerialize();
+
+    void testParts();
+    void testHeaderFetch();
+    void testMultiDeserialize();
+};
+
+#endif
diff --git a/plugins/kaeventformatter.cpp b/plugins/kaeventformatter.cpp
new file mode 100644 (file)
index 0000000..92d8148
--- /dev/null
@@ -0,0 +1,341 @@
+/*
+ *  kaeventformatter.cpp  -  converts KAlarmCal::KAEvent properties to text
+ *  Copyright © 2010,2011 by David Jarvie <djarvie@kde.org>
+ *
+ *  This library is free software; you can redistribute it and/or modify it
+ *  under the terms of the GNU Library General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or (at your
+ *  option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful, but WITHOUT
+ *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ *  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ *  License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public License
+ *  along with this library; see the file COPYING.LIB.  If not, write to the
+ *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ *  02110-1301, USA.
+ */
+
+#include "kaeventformatter.h"
+
+#include <kalarmcal/kacalendar.h>
+#include <kalarmcal/kaevent.h>
+#include <kalarmcal/datetime.h>
+
+#include <kcalutils/incidenceformatter.h>
+
+#include <kglobal.h>
+#include <kdatetime.h>
+#include <KLocale>
+
+static QString trueFalse(bool value);
+static QString number(unsigned long n);
+static QString minutes(int n);
+static QString minutesHoursDays(int minutes);
+static QString dateTime(const KDateTime &);
+
+KAEventFormatter::KAEventFormatter(const KAEvent &e, bool falseForUnspecified)
+    : mEvent(e)
+{
+    if (falseForUnspecified) {
+        mUnspecifiedValue = trueFalse(false);
+    }
+}
+
+QString KAEventFormatter::label(Parameter param)
+{
+    switch (param) {
+    case Id:                return i18nc("@label Unique identifier", "UID");
+    case AlarmType:         return i18nc("@label", "Alarm type");
+    case AlarmCategory:     return i18nc("@label", "Alarm status");
+    case TemplateName:      return i18nc("@label", "Template name");
+    case CreatedTime:       return i18nc("@label", "Creation time");
+    case StartTime:         return i18nc("@label", "Start time");
+    case TemplateAfterTime: return i18nc("@label Start delay configured in an alarm template", "Template after time");
+    case Recurs:            return i18nc("@label", "Recurs");
+    case Recurrence:        return i18nc("@label", "Recurrence");
+    case SubRepetition:     return i18nc("@label", "Sub-repetition");
+    case RepeatInterval:    return i18nc("@label", "Sub-repetition interval");
+    case RepeatCount:       return i18nc("@label", "Sub-repetition count");
+    case NextRepetition:    return i18nc("@label", "Next sub-repetition");
+    case WorkTimeOnly:      return i18nc("@label", "Work time only");
+    case HolidaysExcluded:  return i18nc("@label", "Holidays excluded");
+    case NextRecurrence:    return i18nc("@label", "Next recurrence");
+    case LateCancel:        return i18nc("@label", "Late cancel");
+    case AutoClose:         return i18nc("@label Automatically close window", "Auto close");
+    case CopyKOrganizer:    return i18nc("@label", "Copy to KOrganizer");
+    case Enabled:           return i18nc("@label", "Enabled");
+    case ReadOnly:          return i18nc("@label", "Read-only");
+    case Archive:           return i18nc("@label Whether alarm should be archived", "Archive");
+    case Revision:          return i18nc("@label", "Revision");
+    case CustomProperties:  return i18nc("@label", "Custom properties");
+
+    case MessageText:       return i18nc("@label", "Message text");
+    case MessageFile:       return i18nc("@label File to provide text for message", "Message file");
+    case FgColour:          return i18nc("@label", "Foreground color");
+    case BgColour:          return i18nc("@label", "Background color");
+    case Font:              return i18nc("@label", "Font");
+    case PreAction:         return i18nc("@label Shell command to execute before alarm", "Pre-alarm action");
+    case PreActionCancel:   return i18nc("@label", "Pre-alarm action cancel");
+    case PreActionNoError:  return i18nc("@label", "Pre-alarm action no error");
+    case PostAction:        return i18nc("@label Shell command to execute after alarm", "Post-alarm action");
+    case ConfirmAck:        return i18nc("@label", "Confirm acknowledgement");
+    case KMailSerial:       return i18nc("@label", "KMail serial number");
+    case Sound:             return i18nc("@label Audio method", "Sound");
+    case SoundRepeat:       return i18nc("@label Whether audio should repeat", "Sound repeat");
+    case SoundVolume:       return i18nc("@label", "Sound volume");
+    case SoundFadeVolume:   return i18nc("@label", "Sound fade volume");
+    case SoundFadeTime:     return i18nc("@label", "Sound fade time");
+    case Reminder:          return i18nc("@label Whether the alarm has a reminder", "Reminder");
+    case ReminderOnce:      return i18nc("@label Whether reminder is on first recurrence only", "Reminder once only");
+    case DeferralType:      return i18nc("@label Deferral type", "Deferral");
+    case DeferralTime:      return i18nc("@label", "Deferral time");
+    case DeferDefault:      return i18nc("@label Default deferral delay", "Deferral default");
+    case DeferDefaultDate:  return i18nc("@label Whether deferral time is date-only by default", "Deferral default date only");
+
+    case Command:           return i18nc("@label A shell command", "Command");
+    case LogFile:           return i18nc("@label", "Log file");
+    case CommandXTerm:      return i18nc("@label Execute in terminal window", "Execute in terminal");
+
+    case EmailSubject:      return i18nc("@label", "Email subject");
+    case EmailFromId:       return i18nc("@label Email address", "Email sender ID");
+    case EmailTo:           return i18nc("@label Email address", "Email to");
+    case EmailBcc:          return i18nc("@label true/false", "Email bcc");
+    case EmailBody:         return i18nc("@label", "Email body");
+    case EmailAttachments:  return i18nc("@label", "Email attachments");
+    }
+    return QString();
+}
+
+bool KAEventFormatter::isApplicable(Parameter param) const
+{
+    switch (param) {
+    case Id:
+    case AlarmType:
+    case AlarmCategory:
+    case CreatedTime:
+    case StartTime:
+    case Recurs:
+    case LateCancel:
+    case Enabled:
+    case ReadOnly:
+    case Archive:
+    case Revision:
+    case CustomProperties:
+    case CopyKOrganizer:
+        return true;
+    case TemplateName:
+    case TemplateAfterTime:
+        return mEvent.isTemplate();
+    case Recurrence:
+    case RepeatCount:
+    case SubRepetition:
+    case WorkTimeOnly:
+    case HolidaysExcluded:
+    case NextRecurrence:
+        return mEvent.recurs();
+    case RepeatInterval:
+    case NextRepetition:
+        return mEvent.repetition();
+    case AutoClose:
+        return mEvent.lateCancel();
+
+    case MessageText:
+        return mEvent.actionSubType() == KAEvent::MESSAGE;
+    case MessageFile:
+        return mEvent.actionSubType() == KAEvent::FILE;
+    case FgColour:
+    case BgColour:
+    case Font:
+    case PreAction:
+    case PostAction:
+    case ConfirmAck:
+    case KMailSerial:
+    case Reminder:
+    case DeferralType:
+    case DeferDefault:
+        return mEvent.actionTypes() & KAEvent::ACT_DISPLAY;
+    case ReminderOnce:
+        return mEvent.reminderMinutes() && mEvent.recurs();
+    case DeferralTime:
+        return mEvent.deferred();
+    case DeferDefaultDate:
+        return mEvent.deferDefaultMinutes() > 0;
+    case PreActionCancel:
+    case PreActionNoError:
+        return !mEvent.preAction().isEmpty();
+    case Sound:
+        return mEvent.actionSubType() == KAEvent::MESSAGE  ||  mEvent.actionSubType() == KAEvent::AUDIO;
+    case SoundRepeat:
+        return !mEvent.audioFile().isEmpty();
+    case SoundVolume:
+        return mEvent.soundVolume() >= 0;
+    case SoundFadeVolume:
+    case SoundFadeTime:
+        return mEvent.fadeVolume() >= 0;
+
+    case Command:
+    case LogFile:
+    case CommandXTerm:
+        return mEvent.actionSubType() == KAEvent::COMMAND;
+
+    case EmailSubject:
+    case EmailFromId:
+    case EmailTo:
+    case EmailBcc:
+    case EmailBody:
+    case EmailAttachments:
+        return mEvent.actionSubType() == KAEvent::EMAIL;
+    }
+    return false;
+}
+
+QString KAEventFormatter::value(Parameter param) const
+{
+    switch (param) {
+    case Id:                return mEvent.id();
+    case AlarmType:
+        switch (mEvent.actionSubType()) {
+        case KAEvent::MESSAGE:  return i18nc("@info Alarm type", "Display (text)");
+        case KAEvent::FILE:     return i18nc("@info Alarm type", "Display (file)");
+        case KAEvent::COMMAND:  return mEvent.commandDisplay()
+                                           ? i18nc("@info Alarm type", "Display (command)")
+                                           : i18nc("@info Alarm type", "Command");
+        case KAEvent::EMAIL:    return i18nc("@info Alarm type", "Email");
+        case KAEvent::AUDIO:    return i18nc("@info Alarm type", "Audio");
+        }
+        break;
+    case AlarmCategory:
+        switch (mEvent.category()) {
+        case CalEvent::ACTIVE:    return i18nc("@info Alarm type", "Active");
+        case CalEvent::ARCHIVED:  return i18nc("@info Alarm type", "Archived");
+        case CalEvent::TEMPLATE:  return i18nc("@info Alarm type", "Template");
+        default:
+            break;
+        }
+        break;
+    case TemplateName:      return mEvent.templateName();
+    case CreatedTime:       return mEvent.createdDateTime().toUtc().toString(QStringLiteral("%Y-%m-%d %H:%M:%SZ"));
+    case StartTime:         return dateTime(mEvent.startDateTime().kDateTime());
+    case TemplateAfterTime: return (mEvent.templateAfterTime() >= 0) ? number(mEvent.templateAfterTime()) : trueFalse(false);
+    case Recurs:            return trueFalse(mEvent.recurs());
+    case Recurrence: {
+        if (mEvent.repeatAtLogin(true)) {
+            return i18nc("@info Repeat at login", "At login until %1", dateTime(mEvent.mainDateTime().kDateTime()));
+        }
+        KCalCore::Event::Ptr eptr(new KCalCore::Event);
+        mEvent.updateKCalEvent(eptr, KAEvent::UID_SET);
+        return KCalUtils::IncidenceFormatter::recurrenceString(eptr);
+    }
+    case NextRecurrence:    return dateTime(mEvent.mainDateTime().kDateTime());
+    case SubRepetition:     return trueFalse(mEvent.repetition());
+    case RepeatInterval:    return mEvent.repetitionText(true);
+    case RepeatCount:       return mEvent.repetition() ? number(mEvent.repetition().count()) : QString();
+    case NextRepetition:    return mEvent.repetition() ? number(mEvent.nextRepetition()) : QString();
+    case WorkTimeOnly:      return trueFalse(mEvent.workTimeOnly());
+    case HolidaysExcluded:  return trueFalse(mEvent.holidaysExcluded());
+    case LateCancel:        return mEvent.lateCancel() ? minutesHoursDays(mEvent.lateCancel()) : trueFalse(false);
+    case AutoClose:         return trueFalse(mEvent.lateCancel() ? mEvent.autoClose() : false);
+    case CopyKOrganizer:    return trueFalse(mEvent.copyToKOrganizer());
+    case Enabled:           return trueFalse(mEvent.enabled());
+    case ReadOnly:          return trueFalse(mEvent.isReadOnly());
+    case Archive:           return trueFalse(mEvent.toBeArchived());
+    case Revision:          return number(mEvent.revision());
+    case CustomProperties: {
+        if (mEvent.customProperties().isEmpty()) {
+            return QString();
+        }
+        QString value;
+        const auto customProperties = mEvent.customProperties();
+        for (auto it = customProperties.cbegin(), end = customProperties.cend(); it != end; ++it) {
+            value += QString::fromLatin1(it.key()) + QLatin1String(":") + it.value() + QLatin1String("<nl/>");
+        }
+        return i18nc("@info", "%1", value);
+    }
+
+    case MessageText:       return (mEvent.actionSubType() == KAEvent::MESSAGE) ? mEvent.cleanText() : QString();
+    case MessageFile:       return (mEvent.actionSubType() == KAEvent::FILE) ? mEvent.cleanText() : QString();
+    case FgColour:          return mEvent.fgColour().name();
+    case BgColour:          return mEvent.bgColour().name();
+    case Font:              return mEvent.useDefaultFont() ? i18nc("@info Using default font", "Default") : mEvent.font().toString();
+    case PreActionCancel:   return trueFalse(mEvent.extraActionOptions() & KAEvent::CancelOnPreActError);
+    case PreActionNoError:  return trueFalse(mEvent.extraActionOptions() & KAEvent::CancelOnPreActError);
+    case PreAction:         return mEvent.preAction();
+    case PostAction:        return mEvent.postAction();
+    case Reminder:          return mEvent.reminderMinutes() ? minutesHoursDays(mEvent.reminderMinutes()) : trueFalse(false);
+    case ReminderOnce:      return trueFalse(mEvent.reminderOnceOnly());
+    case DeferralType:      return mEvent.reminderDeferral() ? i18nc("@info", "Reminder") : trueFalse(mEvent.deferred());
+    case DeferralTime:      return mEvent.deferred() ? dateTime(mEvent.deferDateTime().kDateTime()) : trueFalse(false);
+    case DeferDefault:      return (mEvent.deferDefaultMinutes() > 0) ? minutes(mEvent.deferDefaultMinutes()) : trueFalse(false);
+    case DeferDefaultDate:  return trueFalse(mEvent.deferDefaultDateOnly());
+    case ConfirmAck:        return trueFalse(mEvent.confirmAck());
+    case KMailSerial:       return mEvent.kmailSerialNumber() ? number(mEvent.kmailSerialNumber()) : trueFalse(false);
+    case Sound:             return !mEvent.audioFile().isEmpty() ? mEvent.audioFile()
+                                       : mEvent.speak() ? i18nc("@info", "Speak")
+                                       : mEvent.beep() ? i18nc("@info", "Beep") : trueFalse(false);
+    case SoundRepeat:       return trueFalse(mEvent.repeatSound());
+    case SoundVolume:       return mEvent.soundVolume() >= 0
+                                       ? i18nc("@info Percentage", "%1%%", static_cast<int>(mEvent.soundVolume() * 100))
+                                       : mUnspecifiedValue;
+    case SoundFadeVolume:   return mEvent.fadeVolume() >= 0
+                                       ? i18nc("@info Percentage", "%1%%", static_cast<int>(mEvent.fadeVolume() * 100))
+                                       : mUnspecifiedValue;
+    case SoundFadeTime:     return mEvent.fadeSeconds()
+                                       ? i18ncp("@info", "1 Second", "%1 Seconds", mEvent.fadeSeconds())
+                                       : mUnspecifiedValue;
+
+    case Command:           return (mEvent.actionSubType() == KAEvent::COMMAND) ? mEvent.cleanText() : QString();
+    case LogFile:           return mEvent.logFile();
+    case CommandXTerm:      return trueFalse(mEvent.commandXterm());
+
+    case EmailSubject:      return mEvent.emailSubject();
+    case EmailFromId:       return (mEvent.actionSubType() == KAEvent::EMAIL) ? number(mEvent.emailFromId()) : QString();
+    case EmailTo:           return mEvent.emailAddresses(QStringLiteral(", "));
+    case EmailBcc:          return trueFalse(mEvent.emailBcc());
+    case EmailBody:         return mEvent.emailMessage();
+    case EmailAttachments:  return mEvent.emailAttachments(QStringLiteral(", "));
+    }
+    return i18nc("@info Error indication", "error!");
+}
+
+QString trueFalse(bool value)
+{
+    return value ? i18nc("@info General purpose status indication: yes or no", "Yes")
+           : i18nc("@info General purpose status indication: yes or no", "No");
+}
+
+// Convert an integer to digits for the locale.
+// Do not use for date/time or monetary numbers (which have their own digit sets).
+QString number(unsigned long n)
+{
+    KLocale *locale = KLocale::global();
+    return locale->convertDigits(QString::number(n), locale->digitSet());
+}
+
+QString minutes(int n)
+{
+    return i18ncp("@info", "1 Minute", "%1 Minutes", n);
+}
+
+QString dateTime(const KDateTime &dt)
+{
+    if (dt.isDateOnly()) {
+        return dt.toString(QStringLiteral("%Y-%m-%d %:Z"));
+    } else {
+        return dt.toString(QStringLiteral("%Y-%m-%d %H:%M %:Z"));
+    }
+}
+
+QString minutesHoursDays(int minutes)
+{
+    if (minutes % 60) {
+        return i18ncp("@info", "1 Minute", "%1 Minutes", minutes);
+    } else if (minutes % 1440) {
+        return i18ncp("@info", "1 Hour", "%1 Hours", minutes / 60);
+    } else {
+        return i18ncp("@info", "1 Day", "%1 Days", minutes / 1440);
+    }
+}
+
diff --git a/plugins/kaeventformatter.h b/plugins/kaeventformatter.h
new file mode 100644 (file)
index 0000000..ee457f8
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ *  kaeventformatter.h  -  converts KAlarmCal::KAEvent properties to text
+ *  Copyright © 2010-2011 by David Jarvie <djarvie@kde.org>
+ *
+ *  This library is free software; you can redistribute it and/or modify it
+ *  under the terms of the GNU Library General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or (at your
+ *  option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful, but WITHOUT
+ *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ *  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ *  License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public License
+ *  along with this library; see the file COPYING.LIB.  If not, write to the
+ *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ *  02110-1301, USA.
+ */
+
+#ifndef KAEVENTFORMATTER_H
+#define KAEVENTFORMATTER_H
+
+#include <kalarmcal/kaevent.h>
+
+#include <QString>
+
+using namespace KAlarmCal;
+
+class KAEventFormatter
+{
+public:
+    // KAEvent parameter identifiers.
+    // Note that parameters stored in Akonadi attributes are not included.
+    enum Parameter {
+        Id,
+        AlarmType,
+        AlarmCategory,
+        TemplateName,
+        CreatedTime,
+        StartTime,
+        TemplateAfterTime,
+        Recurs,             // does the event recur?
+        Recurrence,
+        NextRecurrence,     // next alarm time excluding repetitions, including reminder/deferral
+        SubRepetition,      // is there a sub-repetition?
+        RepeatInterval,
+        RepeatCount,
+        NextRepetition,     // next repetition count
+        LateCancel,
+        AutoClose,
+        WorkTimeOnly,
+        HolidaysExcluded,
+        CopyKOrganizer,
+        Enabled,
+        ReadOnly,
+        Archive,
+        Revision,
+        CustomProperties,
+
+        MessageText,
+        MessageFile,
+        FgColour,
+        BgColour,
+        Font,
+        PreAction,
+        PreActionCancel,
+        PreActionNoError,
+        PostAction,
+        ConfirmAck,
+        KMailSerial,
+        Sound,
+        SoundRepeat,
+        SoundVolume,
+        SoundFadeVolume,
+        SoundFadeTime,
+        Reminder,
+        ReminderOnce,
+        DeferralType,
+        DeferralTime,
+        DeferDefault,
+        DeferDefaultDate,
+
+        Command,
+        LogFile,
+        CommandXTerm,
+
+        EmailSubject,
+        EmailFromId,
+        EmailTo,
+        EmailBcc,
+        EmailBody,
+        EmailAttachments
+
+    };
+
+    KAEventFormatter() {}
+    KAEventFormatter(const KAEvent &e, bool falseForUnspecified);
+    bool           isApplicable(Parameter) const;
+    QString        value(Parameter) const;
+    const KAEvent &event() const
+    {
+        return mEvent;
+    }
+    static QString label(Parameter);
+
+private:
+    KAEvent mEvent;
+    QString mUnspecifiedValue;
+};
+
+#endif // KAEVENTFORMATTER_H
+
diff --git a/resources/.krazy b/resources/.krazy
new file mode 100644 (file)
index 0000000..0b16e7f
--- /dev/null
@@ -0,0 +1 @@
+SKIP /tests/
diff --git a/resources/CMakeLists.txt b/resources/CMakeLists.txt
new file mode 100644 (file)
index 0000000..c7e343d
--- /dev/null
@@ -0,0 +1,87 @@
+project(resources)
+
+set(AKONADICALENDAR_LIB_VERSION "4.78.0")
+set(KMBOX_LIB_VERSION "4.78.0")
+
+
+find_package(KF5AkonadiCalendar ${AKONADICALENDAR_LIB_VERSION} CONFIG REQUIRED)
+find_package(KF5Mbox ${KMBOX_LIB_VERSION} CONFIG REQUIRED)
+
+# Extra package
+find_package(KF5GAPI "5.1.0" CONFIG)
+
+
+
+# Xsltproc
+find_package(Xsltproc)
+set_package_properties(Xsltproc PROPERTIES DESCRIPTION "XSLT processor from libxslt" TYPE REQUIRED PURPOSE "Required to generate D-Bus interfaces for all Akonadi resources.")
+
+# Libkolab
+find_package(Libkolab 1.0 QUIET CONFIG)
+set_package_properties(Libkolab PROPERTIES DESCRIPTION "libkolab" URL "http://mirror.kolabsys.com/pub/releases" TYPE OPTIONAL PURPOSE "The Kolab Format libraries are required to build the Kolab Groupware Resource")
+
+# Libkolabxml
+find_package(Libkolabxml 1.1 QUIET CONFIG)
+set_package_properties(Libkolabxml PROPERTIES DESCRIPTION "Kolabxml" URL "http://mirror.kolabsys.com/pub/releases" TYPE OPTIONAL PURPOSE "The Kolab XML Format Schema Definitions Library is required to build the Kolab Groupware Resource")
+
+include_directories(
+  ${CMAKE_CURRENT_SOURCE_DIR}/shared/singlefileresource/
+  ${CMAKE_CURRENT_BINARY_DIR}/shared/singlefileresource/
+  ${CMAKE_CURRENT_SOURCE_DIR}/folderarchivesettings/
+)
+
+
+# resource tests
+macro( akonadi_add_resourcetest _testname _script )
+  if ( ${EXECUTABLE_OUTPUT_PATH} )
+    set( _exepath ${EXECUTABLE_OUTPUT_PATH} )
+  else ()
+    set( _exepath ${kdepim-runtime_BINARY_DIR}/resourcetester )
+  endif ()
+  if (WIN32)
+    set(_resourcetester ${_exepath}/resourcetester.bat)
+  else ()
+    set(_resourcetester ${_exepath}/resourcetester)
+  endif ()
+  if (UNIX)
+    set(_resourcetester ${_resourcetester}.shell)
+  endif ()
+  configure_file(${CMAKE_CURRENT_SOURCE_DIR}/${_script} ${CMAKE_CURRENT_BINARY_DIR}/${_script} COPYONLY)
+  if (KDEPIM_RUN_ISOLATED_TESTS)
+    add_test( akonadi-mysql-db-${_testname} akonaditest -c ${kdepim-runtime_SOURCE_DIR}/resourcetester/tests/unittestenv/config-mysql-db.xml ${_resourcetester} -c ${CMAKE_CURRENT_BINARY_DIR}/${_script} )
+  endif ()
+endmacro( akonadi_add_resourcetest )
+
+
+
+add_subdirectory( akonotes )
+add_subdirectory( kalarm )
+add_subdirectory( contacts )
+add_subdirectory( dav )
+add_subdirectory( ical )
+add_subdirectory( imap )
+if (Libkolab_FOUND AND Libkolabxml_FOUND)
+    add_subdirectory( kolab )
+endif()
+
+add_subdirectory( maildir )
+
+add_subdirectory( openxchange )
+add_subdirectory( pop3 )
+
+if( KF5GAPI_FOUND )
+  add_subdirectory( google )
+  # Disabled in KDE 4.14 - too many issues for stable release
+  #add_subdirectory( gmail )
+endif()
+
+add_subdirectory( shared )
+add_subdirectory( birthdays )
+add_subdirectory( mixedmaildir )
+add_subdirectory( mbox )
+add_subdirectory( vcarddir )
+add_subdirectory( icaldir )
+add_subdirectory( vcard )
+add_subdirectory( folderarchivesettings )
+
+
diff --git a/resources/Info.plist.template b/resources/Info.plist.template
new file mode 100644 (file)
index 0000000..c39ddb9
--- /dev/null
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+    <key>CFBundleDevelopmentRegion</key>
+    <string>English</string>
+    <key>CFBundleExecutable</key>
+    <string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
+    <key>CFBundleGetInfoString</key>
+    <string>${MACOSX_BUNDLE_INFO_STRING}</string>
+    <key>CFBundleIconFile</key>
+    <string>${MACOSX_BUNDLE_ICON_FILE}</string>
+    <key>CFBundleIdentifier</key>
+    <string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
+    <key>CFBundleInfoDictionaryVersion</key>
+    <string>6.0</string>
+    <key>CFBundleLongVersionString</key>
+    <string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string>
+    <key>CFBundleName</key>
+    <string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
+    <key>CFBundlePackageType</key>
+    <string>APPL</string>
+    <key>CFBundleShortVersionString</key>
+    <string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
+    <key>CFBundleVersion</key>
+    <string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
+    <key>CSResourcesFileMapped</key>
+    <true/>
+    <key>LSRequiresCarbon</key>
+    <true/>
+    <key>LSUIElement</key>
+    <string>1</string>
+    <key>NSHumanReadableCopyright</key>
+    <string>${MACOSX_BUNDLE_COPYRIGHT}</string>
+</dict>
+</plist>
diff --git a/resources/Mainpage.dox b/resources/Mainpage.dox
new file mode 100644 (file)
index 0000000..d035670
--- /dev/null
@@ -0,0 +1,2 @@
+// DOXYGEN_NAME=Akonadi Resources
+// DOXYGEN_ENABLE=YES
diff --git a/resources/akonotes/CMakeLists.txt b/resources/akonotes/CMakeLists.txt
new file mode 100644 (file)
index 0000000..2f00bb9
--- /dev/null
@@ -0,0 +1,52 @@
+include_directories(
+    ${kdepim-runtime_SOURCE_DIR}
+    ${CMAKE_CURRENT_SOURCE_DIR}/../maildir
+    ${CMAKE_CURRENT_BINARY_DIR}/../maildir
+    ${CMAKE_CURRENT_SOURCE_DIR}/../maildir/libmaildir
+)
+
+
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_akonotes_resource\")
+
+
+########### next target ###############
+
+set( akonotesresource_SRCS
+  ../maildir/maildirresource.cpp
+  ../maildir/configdialog.cpp
+  ../maildir/retrieveitemsjob.cpp
+  ../maildir/maildirresource_debug.cpp
+  akonotesresource.cpp
+)
+
+
+kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/../maildir/maildirresource.kcfg org.kde.Akonadi.Maildir.Settings)
+
+ki18n_wrap_ui(akonotesresource_SRCS ../maildir/settings.ui)
+
+kconfig_add_kcfg_files(akonotesresource_SRCS ../maildir/settings.kcfgc)
+
+qt5_add_dbus_adaptor(akonotesresource_SRCS
+  ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.Maildir.Settings.xml settings.h Akonadi_Maildir_Resource::MaildirSettings maildirsettingsadaptor MaildirSettingsAdaptor
+)
+
+install( FILES akonotesresource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents" )
+
+add_executable(akonadi_akonotes_resource ${akonotesresource_SRCS})
+
+target_link_libraries(akonadi_akonotes_resource
+    KF5::AkonadiCore
+    KF5::AkonadiMime
+    KF5::Mime
+    KF5::AkonadiAgentBase
+    KF5::DBusAddons
+    KF5::I18n
+    KF5::KIOWidgets
+    KF5::WindowSystem
+    KF5::ConfigWidgets
+    maildir
+    folderarchivesettings
+)
+
+install(TARGETS akonadi_akonotes_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
+
diff --git a/resources/akonotes/Messages.sh b/resources/akonotes/Messages.sh
new file mode 100644 (file)
index 0000000..1d9220f
--- /dev/null
@@ -0,0 +1,2 @@
+#! /usr/bin/env bash
+$XGETTEXT *.cpp -o $podir/akonadi_akonotes_resource.pot
diff --git a/resources/akonotes/akonotesresource.cpp b/resources/akonotes/akonotesresource.cpp
new file mode 100644 (file)
index 0000000..7e6240c
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+    Copyright (c) 2007 Till Adam <adam@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "akonotesresource.h"
+
+#include <KLocalizedString>
+
+AkonotesResource::AkonotesResource(const QString &id)
+    : MaildirResource(id)
+{
+}
+
+AkonotesResource::~AkonotesResource()
+{
+}
+
+QString AkonotesResource::itemMimeType() const
+{
+    return QStringLiteral("text/x-vnd.akonadi.note");
+}
+
+void AkonotesResource::configure(WId windowId)
+{
+    MaildirResource::configure(windowId);
+    synchronize(); // heavy to do it in the MaildirResource method, which already has sync on demand working properly
+}
+
+QString AkonotesResource::defaultResourceType()
+{
+    return QStringLiteral("notes");
+}
+
+AKONADI_RESOURCE_MAIN(AkonotesResource)
diff --git a/resources/akonotes/akonotesresource.desktop b/resources/akonotes/akonotesresource.desktop
new file mode 100644 (file)
index 0000000..f0fa8f2
--- /dev/null
@@ -0,0 +1,97 @@
+[Desktop Entry]
+Name=AkoNotes
+Name[bg]=AkoNotes
+Name[bs]=AkoNotes
+Name[ca]=AkoNotes
+Name[ca@valencia]=AkoNotes
+Name[cs]=AkoNotes
+Name[da]=AkoNotes
+Name[de]=AkoNotes
+Name[el]=AkoNotes
+Name[en_GB]=AkoNotes
+Name[es]=AkoNotes
+Name[et]=AkoNotes
+Name[fi]=AkoNotes
+Name[fr]=AkoNotes
+Name[ga]=AkoNotes
+Name[gl]=AkoNotes
+Name[hu]=AkoNotes
+Name[ia]=AkoNotes
+Name[it]=AkoNotes
+Name[ja]=AkoNotes
+Name[kk]=AkoNotes
+Name[km]=AkoNotes
+Name[ko]=AkoNotes
+Name[lt]=AkoNotes
+Name[lv]=AkoNotes
+Name[nb]=AkoNotes
+Name[nds]=Akonotes
+Name[nl]=AkoNotes
+Name[pl]=AkoNotes
+Name[pt]=AkoNotes
+Name[pt_BR]=AkoNotes
+Name[ro]=AkoNote
+Name[ru]=AkoNotes
+Name[sk]=AkoNotes
+Name[sl]=AkoNotes
+Name[sr]=Ако‑белешке
+Name[sr@ijekavian]=Ако‑биљешке
+Name[sr@ijekavianlatin]=Ako‑bilješke
+Name[sr@latin]=Ako‑beleške
+Name[sv]=Ako-anteckningar
+Name[tr]=AkoNot
+Name[uk]=Нотатки
+Name[x-test]=xxAkoNotesxx
+Name[zh_CN]=AkoNotes
+Name[zh_TW]=AkoNotes
+Comment=Loads a notes hierarchy from a local maildir folder
+Comment[ast]=Carga una xerarquía de notes dende ua carpeta de coréu llocal
+Comment[bs]=Učitava hijerarhiju napomena iz loklnog maildir direktorija
+Comment[ca]=Carrega una jerarquia de notes des d'una carpeta pel directori de correu local
+Comment[ca@valencia]=Carrega una jerarquia de notes des d'una carpeta pel directori de correu local
+Comment[da]=Indlæser et hierarki af noter fra en lokal maildir-mappe
+Comment[de]=Laden einer Notizenhierarchie aus einem lokalen Maildir-Ordner
+Comment[el]=Φόρτωση ιεραρχίας σημειώσεων από έναν τοπικό φάκελο maildir
+Comment[en_GB]=Loads a notes hierarchy from a local maildir folder
+Comment[es]=Carga una jerarquía de notas desde una carpeta de directorio de correo local
+Comment[et]=Märkmete hierarhia laadimine kohalikust maildir-kaustast
+Comment[fi]=Lataa muistiinpanohierarkian paikallisesta maildir-kansiosta
+Comment[fr]=Charge une arborescence de notes d'un dossier au format « maildir x
+Comment[ga]=Luchtaíonn sé seo ordlathas nótaí ó fhillteán logánta maildir
+Comment[gl]=Carga unha xerarquía de notas desde un cartafol de correo local
+Comment[hu]=Jegyzethierarchia betöltése egy helyi maildir mappából
+Comment[ia]=Lege un hierarchia de notas de un dossier local de Maildir
+Comment[it]=Carica una gerarchia di note da una cartella locale maildir
+Comment[ja]=ローカルの maildir フォルダから notes の階層を読み込みます
+Comment[kk]=Жергілікті maildir қапшығынан жазбалар иерархиясын жүктеп алады
+Comment[km]=ផ្ទុក​ឋានានុក្រម​ចំណាំ​ពី​ថត maildir មូលដ្ឋាន
+Comment[ko]=로컬 maildir 폴더에서 노트 계층 구조를 불러옴
+Comment[lt]=įkelia užrašų hierarchiją iš vietinio maildir aplanko
+Comment[lv]=Ielādē piezīmju hierarhiju no lokālas maildir mapes
+Comment[nb]=Laster et notat-hierarki fra en lokal maildir-mappe
+Comment[nds]=Notizenstruktuur ut en lokaal Nettpostorner laden
+Comment[nl]=Laadt hiërarchie van notities van een lokale maildir-map
+Comment[pl]=Wczytuje hierarchię notatek z lokalnego katalogu maildir
+Comment[pt]=Carrega uma hierarquia de notas de uma pasta Maildir local
+Comment[pt_BR]=Carrega uma hierarquia de notas de uma pasta maildir local
+Comment[ro]=Încarcă o ierarhie de notițe dintr-un dosar maildir local
+Comment[ru]=Загрузка иерархии примечаний из локальной папки с почтой
+Comment[sk]=Načíta hierarchiu poznámok z miestneho priečinka maildir
+Comment[sl]=Naloži hierarhijo sporočilc iz krajevne poštne mape MailDir
+Comment[sr]=Учитава хијерархију белешки из локалне мејлдир фасцикле
+Comment[sr@ijekavian]=Учитава хијерархију биљешки из локалне мејлдир фасцикле
+Comment[sr@ijekavianlatin]=Učitava hijerarhiju bilješki iz lokalne maildir fascikle
+Comment[sr@latin]=Učitava hijerarhiju beleški iz lokalne maildir fascikle
+Comment[sv]=Laddar en anteckningshierarki från en lokal maildir-katalog
+Comment[tr]=Yerel bir maildir dizininden not hiyerarşisini yükler
+Comment[uk]=Завантажує ієрархію нотаток з локальної теки maildir
+Comment[x-test]=xxLoads a notes hierarchy from a local maildir folderxx
+Comment[zh_CN]=从本地 maildir 文件夹中载入层次型便笺
+Comment[zh_TW]=從本地 Maildir 格式的目錄中載入組織便條
+Type=AkonadiResource
+Exec=akonadi_akonotes_resource
+Icon=view-pim-notes
+
+X-Akonadi-MimeTypes=text/x-vnd.akonadi.note
+X-Akonadi-Capabilities=Resource,Notes
+X-Akonadi-Identifier=akonadi_akonotes_resource
diff --git a/resources/akonotes/akonotesresource.h b/resources/akonotes/akonotesresource.h
new file mode 100644 (file)
index 0000000..100d009
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+    Copyright (c) 2007 Till Adam <adam@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef __AKONOTES_RESOURCE_H__
+#define __AKONOTES_RESOURCE_H__
+
+#include "maildirresource.h"
+
+class AkonotesResource : public MaildirResource
+{
+    Q_OBJECT
+public:
+    explicit AkonotesResource(const QString &id);
+    ~AkonotesResource();
+
+    QString defaultResourceType() Q_DECL_OVERRIDE;
+public Q_SLOTS:
+    void configure(WId windowId) Q_DECL_OVERRIDE;
+
+protected:
+    QString itemMimeType() const Q_DECL_OVERRIDE;
+};
+#endif
diff --git a/resources/birthdays/CMakeLists.txt b/resources/birthdays/CMakeLists.txt
new file mode 100644 (file)
index 0000000..823d6f0
--- /dev/null
@@ -0,0 +1,37 @@
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_birthdays_resource\")
+
+set( birthdayresource_srcs
+  birthdaysresource.cpp
+  configdialog.cpp
+)
+
+kconfig_add_kcfg_files( birthdayresource_srcs settings.kcfgc )
+kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/birthdaysresource.kcfg org.kde.Akonadi.Birthdays.Settings)
+
+ki18n_wrap_ui(birthdayresource_srcs configdialog.ui)
+
+qt5_add_dbus_adaptor(birthdayresource_srcs
+  ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.Birthdays.Settings.xml settings.h Settings
+)
+
+ecm_qt_declare_logging_category(birthdayresource_srcs HEADER birthdays_debug.h IDENTIFIER BIRTHDAYS_LOG CATEGORY_NAME log_birthdays)
+
+add_executable(akonadi_birthdays_resource ${birthdayresource_srcs})
+
+if( APPLE )
+  set_target_properties(akonadi_birthdays_resource PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../Info.plist.template)
+  set_target_properties(akonadi_birthdays_resource PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.Birthdays")
+  set_target_properties(akonadi_birthdays_resource PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi Birthdays Resource")
+endif ()
+
+target_link_libraries(akonadi_birthdays_resource
+  KF5::AkonadiCore
+  KF5::CalendarCore
+  KF5::Codecs
+  KF5::AkonadiAgentBase
+  KF5::Contacts
+  KF5::AkonadiWidgets
+)
+
+install( TARGETS akonadi_birthdays_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} )
+install( FILES birthdaysresource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents" )
diff --git a/resources/birthdays/Messages.sh b/resources/birthdays/Messages.sh
new file mode 100644 (file)
index 0000000..9a63721
--- /dev/null
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+$EXTRACTRC `find . -name \*.ui` >> rc.cpp || exit 11
+$XGETTEXT *.cpp -o $podir/akonadi_birthdays_resource.pot
diff --git a/resources/birthdays/birthdaysresource.cpp b/resources/birthdays/birthdaysresource.cpp
new file mode 100644 (file)
index 0000000..0b28aee
--- /dev/null
@@ -0,0 +1,365 @@
+/*
+    Copyright (c) 2003 Cornelius Schumacher <schumacher@kde.org>
+    Copyright (c) 2009 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "birthdaysresource.h"
+#include "settings.h"
+#include "settingsadaptor.h"
+#include "configdialog.h"
+
+#include <collectionfetchjob.h>
+#include <itemfetchjob.h>
+#include <itemfetchscope.h>
+#include <mimetypechecker.h>
+#include <monitor.h>
+#include <entitydisplayattribute.h>
+#include <AkonadiCore/vectorhelper.h>
+
+#include <kcontacts/addressee.h>
+
+#include <KCodecs/KEmailAddress>
+
+#include "birthdays_debug.h"
+#include <KLocalizedString>
+#include <KWindowSystem>
+
+#include <AkonadiCore/TagCreateJob>
+
+using namespace Akonadi;
+using namespace KContacts;
+using namespace KCalCore;
+
+BirthdaysResource::BirthdaysResource(const QString &id) :
+    ResourceBase(id)
+{
+    new SettingsAdaptor(Settings::self());
+    QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"),
+            Settings::self(), QDBusConnection::ExportAdaptors);
+
+    setName(i18n("Birthdays & Anniversaries"));
+
+    Monitor *monitor = new Monitor(this);
+    monitor->setMimeTypeMonitored(Addressee::mimeType());
+    monitor->itemFetchScope().fetchFullPayload();
+    connect(monitor, &Monitor::itemAdded, this, &BirthdaysResource::contactChanged);
+    connect(monitor, &Monitor::itemChanged, this, &BirthdaysResource::contactChanged);
+    connect(monitor, &Monitor::itemRemoved, this, &BirthdaysResource::contactRemoved);
+
+    connect(this, &BirthdaysResource::reloadConfiguration, this, &BirthdaysResource::doFullSearch);
+}
+
+BirthdaysResource::~BirthdaysResource()
+{
+}
+
+void BirthdaysResource::configure(WId windowId)
+{
+    ConfigDialog dlg;
+    if (windowId) {
+        KWindowSystem::setMainWindow(&dlg, windowId);
+    }
+    if (dlg.exec()) {
+        Q_EMIT configurationDialogAccepted();
+    } else {
+        Q_EMIT configurationDialogRejected();
+    }
+    doFullSearch();
+    synchronizeCollectionTree();
+}
+
+void BirthdaysResource::retrieveCollections()
+{
+    Collection c;
+    c.setParentCollection(Collection::root());
+    c.setRemoteId(QStringLiteral("akonadi_birthdays_resource"));
+    c.setName(name());
+    c.setContentMimeTypes(QStringList() << QStringLiteral("application/x-vnd.akonadi.calendar.event"));
+    c.setRights(Collection::ReadOnly);
+
+    EntityDisplayAttribute *attribute = c.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
+    attribute->setIconName(QStringLiteral("view-calendar-birthday"));
+
+    Collection::List list;
+    list << c;
+    collectionsRetrieved(list);
+}
+
+void BirthdaysResource::retrieveItems(const Akonadi::Collection &collection)
+{
+    Q_UNUSED(collection);
+    itemsRetrievedIncremental(Akonadi::valuesToVector(mPendingItems), Akonadi::valuesToVector(mDeletedItems));
+    mPendingItems.clear();
+    mDeletedItems.clear();
+}
+
+bool BirthdaysResource::retrieveItem(const Akonadi::Item &item, const QSet< QByteArray > &parts)
+{
+    Q_UNUSED(parts);
+    qint64 contactId = item.remoteId().mid(1).toLongLong();
+    ItemFetchJob *job = new ItemFetchJob(Item(contactId), this);
+    job->fetchScope().fetchFullPayload();
+    connect(job, &ItemFetchJob::result, this, &BirthdaysResource::contactRetrieved);
+    return true;
+}
+
+void BirthdaysResource::contactRetrieved(KJob *job)
+{
+    ItemFetchJob *fj = static_cast<ItemFetchJob *>(job);
+    if (job->error()) {
+        Q_EMIT error(job->errorText());
+        cancelTask();
+    } else if (fj->items().count() != 1) {
+        cancelTask();
+    } else {
+        KCalCore::Incidence::Ptr ev;
+        if (currentItem().remoteId().startsWith(QLatin1Char('b'))) {
+            ev = createBirthday(fj->items().at(0));
+        } else if (currentItem().remoteId().startsWith(QLatin1Char('a'))) {
+            ev = createAnniversary(fj->items().at(0));
+        }
+        if (!ev) {
+            cancelTask();
+        } else {
+            Item i(currentItem());
+            i.setPayload<Incidence::Ptr>(ev);
+            itemRetrieved(i);
+        }
+    }
+}
+
+void BirthdaysResource::contactChanged(const Akonadi::Item &item)
+{
+    if (!item.hasPayload<KContacts::Addressee>()) {
+        return;
+    }
+
+    KContacts::Addressee contact = item.payload<KContacts::Addressee>();
+
+    if (Settings::self()->filterOnCategories()) {
+        bool hasCategory = false;
+        const QStringList categories = contact.categories();
+        foreach (const QString &cat, Settings::self()->filterCategories()) {
+            if (categories.contains(cat)) {
+                hasCategory = true;
+                break;
+            }
+        }
+
+        if (!hasCategory) {
+            return;
+        }
+    }
+
+    Event::Ptr event = createBirthday(item);
+    if (event) {
+        addPendingEvent(event, QStringLiteral("b%1").arg(item.id()));
+    } else {
+        Item i(KCalCore::Event::eventMimeType());
+        i.setRemoteId(QStringLiteral("b%1").arg(item.id()));
+        mDeletedItems[ i.remoteId() ] = i;
+    }
+
+    event = createAnniversary(item);
+    if (event) {
+        addPendingEvent(event, QStringLiteral("a%1").arg(item.id()));
+    } else {
+        Item i(KCalCore::Event::eventMimeType());
+        i.setRemoteId(QStringLiteral("a%1").arg(item.id()));
+        mDeletedItems[ i.remoteId() ] = i;
+    }
+    synchronize();
+}
+
+void BirthdaysResource::addPendingEvent(const KCalCore::Event::Ptr &event, const QString &remoteId)
+{
+    KCalCore::Incidence::Ptr evptr(event);
+    Item i(KCalCore::Event::eventMimeType());
+    i.setRemoteId(remoteId);
+    i.setPayload(evptr);
+    mPendingItems[ remoteId ] = i;
+}
+
+void BirthdaysResource::contactRemoved(const Akonadi::Item &item)
+{
+    Item i(KCalCore::Event::eventMimeType());
+    i.setRemoteId(QStringLiteral("b%1").arg(item.id()));
+    mDeletedItems[ i.remoteId() ] = i;
+    i.setRemoteId(QStringLiteral("a%1").arg(item.id()));
+    mDeletedItems[ i.remoteId() ] = i;
+    synchronize();
+}
+
+void BirthdaysResource::doFullSearch()
+{
+    CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, this);
+    connect(job, &CollectionFetchJob::collectionsReceived, this, &BirthdaysResource::listContacts);
+}
+
+void BirthdaysResource::listContacts(const Akonadi::Collection::List &cols)
+{
+    MimeTypeChecker contactFilter;
+    contactFilter.addWantedMimeType(Addressee::mimeType());
+    foreach (const Collection &col, cols) {
+        if (!contactFilter.isWantedCollection(col)) {
+            continue;
+        }
+        ItemFetchJob *job = new ItemFetchJob(col, this);
+        job->fetchScope().fetchFullPayload();
+        connect(job, &ItemFetchJob::itemsReceived, this, &BirthdaysResource::createEvents);
+    }
+}
+
+void BirthdaysResource::createEvents(const Akonadi::Item::List &items)
+{
+    foreach (const Item &item, items) {
+        contactChanged(item);
+    }
+}
+
+KCalCore::Event::Ptr BirthdaysResource::createBirthday(const Akonadi::Item &contactItem)
+{
+    if (!contactItem.hasPayload<KContacts::Addressee>()) {
+        return KCalCore::Event::Ptr();
+    }
+    KContacts::Addressee contact = contactItem.payload<KContacts::Addressee>();
+
+    const QString name = contact.realName().isEmpty() ? contact.nickName() : contact.realName();
+    if (name.isEmpty()) {
+        qCDebug(BIRTHDAYS_LOG) << "contact " << contact.uid() << contactItem.id() << " has no name, skipping.";
+        return KCalCore::Event::Ptr();
+    }
+
+    const QDate birthdate = contact.birthday().date();
+    if (birthdate.isValid()) {
+        const QString summary = i18n("%1's birthday", name);
+
+        Event::Ptr ev = createEvent(birthdate);
+        ev->setUid(contact.uid() + QStringLiteral("_KABC_Birthday"));
+
+        ev->setCustomProperty("KABC", "BIRTHDAY", QStringLiteral("YES"));
+        ev->setCustomProperty("KABC", "UID-1", contact.uid());
+        ev->setCustomProperty("KABC", "NAME-1", name);
+        ev->setCustomProperty("KABC", "EMAIL-1", contact.preferredEmail());
+        ev->setSummary(summary);
+
+        checkForUnknownCategories(i18n("Birthday"), ev);
+        return ev;
+    }
+    return KCalCore::Event::Ptr();
+}
+
+KCalCore::Event::Ptr BirthdaysResource::createAnniversary(const Akonadi::Item &contactItem)
+{
+    if (!contactItem.hasPayload<KContacts::Addressee>()) {
+        return KCalCore::Event::Ptr();
+    }
+    KContacts::Addressee contact = contactItem.payload<KContacts::Addressee>();
+
+    const QString name = contact.realName().isEmpty() ? contact.nickName() : contact.realName();
+    if (name.isEmpty()) {
+        qCDebug(BIRTHDAYS_LOG) << "contact " << contact.uid() << contactItem.id() << " has no name, skipping.";
+        return KCalCore::Event::Ptr();
+    }
+
+    const QString anniversary_string = contact.custom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Anniversary"));
+    if (anniversary_string.isEmpty()) {
+        return KCalCore::Event::Ptr();
+    }
+    const QDate anniversary = QDate::fromString(anniversary_string, Qt::ISODate);
+    if (anniversary.isValid()) {
+        const QString spouseName = contact.custom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-SpousesName"));
+
+        QString summary;
+        if (!spouseName.isEmpty()) {
+            QString tname, temail;
+            KEmailAddress::extractEmailAddressAndName(spouseName, temail, tname);
+            tname = KEmailAddress::quoteNameIfNecessary(tname);
+            if ((tname[0] == QLatin1Char('"')) && (tname[tname.length() - 1] == QLatin1Char('"'))) {
+                tname.remove(0, 1);
+                tname.truncate(tname.length() - 1);
+            }
+            tname.remove(QLatin1Char('\\'));   // remove escape chars
+            KContacts::Addressee spouse;
+            spouse.setNameFromString(tname);
+            QString name_2 = spouse.nickName();
+            if (name_2.isEmpty()) {
+                name_2 = spouse.realName();
+            }
+            summary = i18nc("insert names of both spouses",
+                            "%1's & %2's anniversary", name, name_2);
+        } else {
+            summary = i18nc("only one spouse in addressbook, insert the name",
+                            "%1's anniversary", name);
+        }
+
+        Event::Ptr event = createEvent(anniversary);
+        event->setUid(contact.uid() + QStringLiteral("_KABC_Anniversary"));
+        event->setSummary(summary);
+
+        event->setCustomProperty("KABC", "UID-1", contact.uid());
+        event->setCustomProperty("KABC", "NAME-1", name);
+        event->setCustomProperty("KABC", "EMAIL-1", contact.fullEmail());
+        event->setCustomProperty("KABC", "ANNIVERSARY", QStringLiteral("YES"));
+        // insert category
+        checkForUnknownCategories(i18n("Anniversary"), event);
+        return event;
+    }
+    return KCalCore::Event::Ptr();
+}
+
+KCalCore::Event::Ptr BirthdaysResource::createEvent(const QDate &date)
+{
+    Event::Ptr event(new Event());
+    event->setDtStart(KDateTime(date, KDateTime::Spec(KDateTime::ClockTime)));
+    event->setDtEnd(KDateTime(date, KDateTime::Spec(KDateTime::ClockTime)));
+    event->setAllDay(true);
+    event->setTransparency(Event::Transparent);
+
+    // Set the recurrence
+    Recurrence *recurrence = event->recurrence();
+    recurrence->setStartDateTime(KDateTime(date, KDateTime::Spec(KDateTime::ClockTime)));
+    recurrence->setYearly(1);
+    if (date.month() == 2 && date.day() == 29) {
+        recurrence->addYearlyDay(60);
+    }
+
+    // Set the alarm
+    event->clearAlarms();
+    if (Settings::self()->enableAlarm()) {
+        Alarm::Ptr alarm = event->newAlarm();
+        alarm->setType(Alarm::Display);
+        alarm->setText(event->summary());
+        alarm->setTime(KDateTime(date, KDateTime::Spec(KDateTime::ClockTime)));
+        // N days before
+        alarm->setStartOffset(Duration(-Settings::self()->alarmDays(), Duration::Days));
+        alarm->setEnabled(true);
+    }
+
+    return event;
+}
+
+void BirthdaysResource::checkForUnknownCategories(const QString &categoryToCheck, Event::Ptr &event)
+{
+    Akonadi::TagCreateJob *tagCreateJob = new Akonadi::TagCreateJob(Akonadi::Tag(categoryToCheck), this);
+    tagCreateJob->setMergeIfExisting(true);
+    event->setCategories(categoryToCheck);
+}
+
+AKONADI_RESOURCE_MAIN(BirthdaysResource)
+
diff --git a/resources/birthdays/birthdaysresource.desktop b/resources/birthdays/birthdaysresource.desktop
new file mode 100644 (file)
index 0000000..43854ec
--- /dev/null
@@ -0,0 +1,100 @@
+[Desktop Entry]
+Name=Birthdays & Anniversaries
+Name[ar]=أعياد الميلاد و الذكريات
+Name[bg]=Рождени дни и годишнини
+Name[bs]=Rođendani i godišnjice
+Name[ca]=Dates de naixement i aniversaris
+Name[ca@valencia]=Dates de naixement i aniversaris
+Name[cs]=Narozeniny a výročí
+Name[da]=Fødselsdage og årsdage
+Name[de]=Geburtstage und Jahrestage
+Name[el]=Γενέθλια & επέτειοι
+Name[en_GB]=Birthdays & Anniversaries
+Name[es]=Cumpleaños y aniversarios
+Name[et]=Sünni- ja aastapäevad
+Name[fi]=Syntymä- ja vuosipäivät
+Name[fr]=Anniversaires et fêtes
+Name[gl]=Cumpreanos e aniversarios
+Name[hu]=Születésnapok és évfordulók
+Name[ia]=Dies natal e anniversarios
+Name[it]=Compleanni e anniversari
+Name[ja]=誕生日と記念日
+Name[kk]=Тұған күн мен жылдықтар
+Name[km]=បុណ្យ​ខួប និង​បុណ្យ​ខួប​កំណើត
+Name[ko]=생일과 기념일
+Name[lt]=Gimtadieniai ir sukaktys
+Name[lv]=Dzimšanas un svētku dienas
+Name[nb]=Vis fødselsdager og bryllupsdager
+Name[nds]=Geboorts- un Johrdaag
+Name[nl]=Verjaardagen & trouwdagen
+Name[nn]=Fødselsdagar og bryllaupsdagar
+Name[pa]=ਜਨਮਦਿਨ ਅਤੇ ਵਰ੍ਹੇਗੰਢ
+Name[pl]=Urodziny i rocznice
+Name[pt]=Datas de Nascimento & Aniversários
+Name[pt_BR]=Aniversários e aniversários de casamento
+Name[ro]=Zile de naștere și aniversări
+Name[ru]=Дни рождения и годовщины свадеб
+Name[sk]=Narodeniny a výročia
+Name[sl]=Rojstni dnevi in obletnice
+Name[sq]=Ditëlindje & Përvjetorë
+Name[sr]=Рођендани и годишњице
+Name[sr@ijekavian]=Рођендани и годишњице
+Name[sr@ijekavianlatin]=Rođendani i godišnjice
+Name[sr@latin]=Rođendani i godišnjice
+Name[sv]=Födelsedagar och årsdagar
+Name[tr]=Doğum günleri & Yıl dönümleri
+Name[ug]=تۇغۇلغان كۈن ۋە خاتىرە كۈنلەر
+Name[uk]=Дні народження і річниці весіль
+Name[x-test]=xxBirthdays & Anniversariesxx
+Name[zh_CN]=生日和纪念日
+Name[zh_TW]=生日與紀念日
+Comment=Provides access to birthday and anniversary dates of contacts in your address book as calendar events
+Comment[ar]=يوفر الحصول على موعد الذكرى السنوية لميلاد أو ذكرى لقائمة دفتر العناوين الخاص بك حسب الجدول الزمني للأحداث
+Comment[bs]=Omogućava pristup datumima rođendana i godišnjica u vašem adresaru kao događajima na kalendaru
+Comment[ca]=Proporciona accés a les dates de naixement i els aniversaris dels contactes a la vostra llibreta d'adreces com a esdeveniments de calendari
+Comment[ca@valencia]=Proporciona accés a les dates de naixement i els aniversaris dels contactes a la vostra llibreta d'adreces com a esdeveniments de calendari
+Comment[da]=Giver adgang til datoer for fødsels- og årsdage for kontakter i din adressebog som kalenderbegivenheder
+Comment[de]=Ermöglicht den Zugriff auf Geburtstage und Jahrestage von Kontakten aus dem KDE-Adressbuch in Form von Kalendereinträgen.
+Comment[el]=Προσφέρει πρόσβαση σε γενέθλια και επετείους επαφών του βιβλίου διευθύνσεών σας ως γεγονότα ημερολογίου
+Comment[en_GB]=Provides access to birthday and anniversary dates of contacts in your address book as calendar events
+Comment[es]=Proporciona acceso a las fechas de cumpleaños y aniversarios de los contactos en la libreta de direcciones como eventos de calendario
+Comment[et]=Võimaldab kasutada KDE aadressiraamatusse kalendrisündmustena salvestatud kontaktide sünni- ja aastapäevi
+Comment[fi]=Noutaa yhteystietojesi syntymä- ja vuosipäivät osoitekirjastasi kalenteritapahtumina
+Comment[fr]=Fournit l'accès aux dates d'anniversaires et de fêtes des contacts du carnet d'adresses KDE comme des évènements de l'agenda
+Comment[gl]=Fornece acceso ás datas de cumpreanos e aniversarios dos contactos do caderno de enderezos como actividades do calendario
+Comment[hu]=Hozzáférést biztosít a névjegyekben tárolt születésnapokhoz és évfordulókhoz a címjegyzékekben (naptári eseményként)
+Comment[ia]=Provide accesso a datos de dies natal e anniversarios de contactos in tu adressario como eventos in le calendario
+Comment[it]=Consente l'accesso alle date di nascita ed agli anniversari dei contatti della rubrica di KDE come eventi di calendario.
+Comment[ja]=アドレス帳にカレンダーイベントとして保存されている誕生日と記念日へのアクセスを提供します
+Comment[kk]=Адрестік кітапшаңыздағы түған күн және жылдықтар мәліметіне күнтізбе оқиғалар ретінде қатынау мүмкіндігін береді
+Comment[km]=ផ្ដល់​សិទ្ធិ​ចូល​ដំណើរការ​ទៅកាន់​កាលបរិច្ឆេទ​ថ្ងៃ​ខួប​កំណើត​នៃ​ទំនាក់ទំនង​នៅ​ក្នុង​សៀវភៅ​អាសយដ្ឋាន KDE ជា​ព្រឹត្តិការណ៍​ប្រតិទិន
+Comment[ko]=주소록에 등록된 생일과 기념일을 달력에서 접근할 수 있도록 함
+Comment[lt]=Suteikia prieigą prie kontaktų gimimo dienų ir sukakčių datų, įrašytų adresų knygelėje, kaip prie kalendoriaus įvykių
+Comment[lv]=Nodrošina piekļuvi KDE adrešu grāmatas kontaktu dzimšanas dienu datumiem kā kalendāra notikumiem
+Comment[nb]=Gir tilgang til bursdager og bryllupsdager for kontakter i adresseboka, som kalenderhendelser
+Comment[nds]=Stellt Togriep op Geboorts- un Johrdaag vun Kontakten binnen Dien Adressbook as Kalenner-Begeevnissen praat
+Comment[nl]=Geeft toegang tot verjaar- en trouwdagdata van contactpersonen in uw adresboek als agenda-gebeurtenissen
+Comment[nn]=Gjev tilgang til fødselsdagane og bryllaupsdagane til kontaktar i adresseboka som kalenderhendingar
+Comment[pl]=Zapewnia dostęp do dat urodzin i rocznic z wizytówek w książce adresowej jako zdarzeń kalendarza
+Comment[pt]=Oferece o acesso às datas de nascimento e aniversário dos contactos no livro de endereços como eventos do calendário
+Comment[pt_BR]=Fornece acesso às datas de aniversário e de casamento de contatos do seu livro de endereços como eventos de calendário
+Comment[ru]=Доступ к информации о днях рождения и годовщинах свадеб из адресной книги KDE как к календарю с событиями
+Comment[sk]=Poskytuje prístup k narodeninám a výročiam kontaktov vo vašom adresári ako udalosti kalendára
+Comment[sl]=Omogoča dostop do rojstnih dni in obletnic stikov, shranjenih v vašem imeniku. Na voljo so kot koledarski dogodki.
+Comment[sr]=Омогућава приступ датумима рођендана и годишњица контаката у вашем адресару као календарским догађајима
+Comment[sr@ijekavian]=Омогућава приступ датумима рођендана и годишњица контаката у вашем адресару као календарским догађајима
+Comment[sr@ijekavianlatin]=Omogućava pristup datumima rođendana i godišnjica kontakata u vašem adresaru kao kalendarskim događajima
+Comment[sr@latin]=Omogućava pristup datumima rođendana i godišnjica kontakata u vašem adresaru kao kalendarskim događajima
+Comment[sv]=Ger tillgång till datum för födelsedagar och årsdagar för kontakter i KDE:s adressbok som kalenderhändelser
+Comment[tr]=Adres defterindeki kişilerin doğum günleri ve yıl dönümlerine takvim olayları olarak erişmeyi sağlar
+Comment[uk]="Надає доступ до дат днів народження і річниць весіль, які зберігаються у вашій адресній книзі як події календаря"
+Comment[x-test]=xxProvides access to birthday and anniversary dates of contacts in your address book as calendar eventsxx
+Comment[zh_CN]=提供对存储在 KDE 地址簿文件夹中的联系人生日和纪念日的访问支持,并将其作为日历中的事件
+Comment[zh_TW]=提供存取儲存通訊錄中的生日與紀念日日期,做為行事曆的事件
+Type=AkonadiResource
+Exec=akonadi_birthdays_resource
+Icon=view-calendar-birthday
+
+X-Akonadi-MimeTypes=text/calendar,application/x-vnd.akonadi.calendar.event
+X-Akonadi-Capabilities=Resource,Unique
+X-Akonadi-Identifier=akonadi_birthdays_resource
diff --git a/resources/birthdays/birthdaysresource.h b/resources/birthdays/birthdaysresource.h
new file mode 100644 (file)
index 0000000..d476b1e
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+    Copyright (c) 2003 Cornelius Schumacher <schumacher@kde.org>
+    Copyright (c) 2009 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef BIRTHDAYSRESOURCE_H
+#define BIRTHDAYSRESOURCE_H
+
+#include <KCalCore/Event>
+
+#include <resourcebase.h>
+
+#include <QHash>
+
+class QDate;
+
+class BirthdaysResource : public Akonadi::ResourceBase, public Akonadi::AgentBase::Observer
+{
+    Q_OBJECT
+
+public:
+    explicit BirthdaysResource(const QString &id);
+    ~BirthdaysResource();
+
+public Q_SLOTS:
+    void configure(WId windowId) Q_DECL_OVERRIDE;
+
+protected:
+    void retrieveCollections() Q_DECL_OVERRIDE;
+    void retrieveItems(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    bool retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+
+private:
+    void addPendingEvent(const KCalCore::Event::Ptr &event, const QString &remoteId);
+    void checkForUnknownCategories(const QString &categoryToCheck, KCalCore::Event::Ptr &event);
+
+    KCalCore::Event::Ptr createBirthday(const Akonadi::Item &contactItem);
+    KCalCore::Event::Ptr createAnniversary(const Akonadi::Item &contactItem);
+    KCalCore::Event::Ptr createEvent(const QDate &date);
+
+private Q_SLOTS:
+    void doFullSearch();
+    void listContacts(const Akonadi::Collection::List &cols);
+    void createEvents(const Akonadi::Item::List &items);
+
+    void contactChanged(const Akonadi::Item &item);
+    void contactRemoved(const Akonadi::Item &item);
+
+    void contactRetrieved(KJob *job);
+private:
+    QHash<QString, Akonadi::Item> mPendingItems;
+    QHash<QString, Akonadi::Item> mDeletedItems;
+};
+
+#endif
diff --git a/resources/birthdays/birthdaysresource.kcfg b/resources/birthdays/birthdaysresource.kcfg
new file mode 100644 (file)
index 0000000..ae2f217
--- /dev/null
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:kcfg="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
+      http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
+
+  <kcfgfile/>
+
+  <group name="Filter">
+    <entry type="Bool" name="FilterOnCategories">
+      <default>false</default>
+    </entry>
+    <entry type="StringList" name="FilterCategories"/>
+  </group>
+
+  <group name="Alarm">
+    <entry type="Bool" name="EnableAlarm">
+      <default>true</default>
+    </entry>
+    <entry type="Int" name="AlarmDays">
+      <default>1</default>
+    </entry>
+  </group>
+</kcfg>
diff --git a/resources/birthdays/configdialog.cpp b/resources/birthdays/configdialog.cpp
new file mode 100644 (file)
index 0000000..19fad2c
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+    Copyright (c) 2003 Cornelius Schumacher <schumacher@kde.org>
+    Copyright (c) 2009 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "configdialog.h"
+#include "settings.h"
+#include <AkonadiCore/Tag>
+#include <kconfigdialogmanager.h>
+#include <QIcon>
+#include <KLocalizedString>
+#include <QPushButton>
+#include <QDialogButtonBox>
+
+ConfigDialog::ConfigDialog(QWidget *parent)
+    : QDialog(parent)
+{
+    QWidget *mainWidget = new QWidget(this);
+
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
+    okButton->setDefault(true);
+    okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
+    connect(buttonBox, &QDialogButtonBox::accepted, this, &ConfigDialog::accept);
+    connect(buttonBox, &QDialogButtonBox::rejected, this, &ConfigDialog::reject);
+    okButton->setDefault(true);
+
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    setLayout(mainLayout);
+    mainLayout->addWidget(mainWidget);
+    mainLayout->addWidget(buttonBox);
+    ui.setupUi(mainWidget);
+    setWindowIcon(QIcon::fromTheme(QStringLiteral("view-calendar-birthday")));
+    mManager = new KConfigDialogManager(this, Settings::self());
+    mManager->updateWidgets();
+    ui.kcfg_AlarmDays->setSuffix(ki18np(" day", " days"));
+
+    connect(okButton, &QPushButton::clicked, this, &ConfigDialog::save);
+    loadTags();
+    readConfig();
+}
+
+ConfigDialog::~ConfigDialog()
+{
+    writeConfig();
+}
+
+void ConfigDialog::loadTags()
+{
+    const QStringList categories = Settings::self()->filterCategories();
+    ui.FilterCategories->setSelectionFromStringList(categories);
+}
+
+void ConfigDialog::save()
+{
+    mManager->updateSettings();
+
+    Settings::self()->setFilterCategories(ui.FilterCategories->tagToStringList());
+    Settings::self()->save();
+}
+
+void ConfigDialog::readConfig()
+{
+    KConfigGroup group(KSharedConfig::openConfig(), "ConfigDialog");
+    const QSize size = group.readEntry("Size", QSize(600, 400));
+    if (size.isValid()) {
+        resize(size);
+    }
+}
+
+void ConfigDialog::writeConfig()
+{
+    KConfigGroup group(KSharedConfig::openConfig(), "ConfigDialog");
+    group.writeEntry("Size", size());
+    group.sync();
+}
+
diff --git a/resources/birthdays/configdialog.h b/resources/birthdays/configdialog.h
new file mode 100644 (file)
index 0000000..0f038c5
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+    Copyright (c) 2003 Cornelius Schumacher <schumacher@kde.org>
+    Copyright (c) 2009 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef CONFIGDIALOG_H
+#define CONFIGDIALOG_H
+
+#include "ui_configdialog.h"
+
+#include <QDialog>
+
+class KConfigDialogManager;
+
+class ConfigDialog : public QDialog
+{
+    Q_OBJECT
+public:
+    explicit ConfigDialog(QWidget *parent = Q_NULLPTR);
+    ~ConfigDialog();
+
+private Q_SLOTS:
+    void save();
+
+private:
+    void loadTags();
+    void readConfig();
+    void writeConfig();
+    Ui::ConfigDialog ui;
+    KConfigDialogManager *mManager;
+};
+
+#endif
diff --git a/resources/birthdays/configdialog.ui b/resources/birthdays/configdialog.ui
new file mode 100644 (file)
index 0000000..7f05d8e
--- /dev/null
@@ -0,0 +1,172 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ConfigDialog</class>
+ <widget class="QWidget" name="ConfigDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>434</width>
+    <height>364</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_2">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QGroupBox" name="groupBox">
+     <property name="title">
+      <string>Reminder</string>
+     </property>
+     <property name="checkable">
+      <bool>false</bool>
+     </property>
+     <layout class="QGridLayout" name="gridLayout">
+      <item row="0" column="0">
+       <widget class="QCheckBox" name="kcfg_EnableAlarm">
+        <property name="text">
+         <string>Set &amp;reminder</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="0">
+       <widget class="QLabel" name="label">
+        <property name="enabled">
+         <bool>false</bool>
+        </property>
+        <property name="text">
+         <string>&amp;Remind prior to event:</string>
+        </property>
+        <property name="buddy">
+         <cstring>kcfg_AlarmDays</cstring>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="1">
+       <widget class="KPluralHandlingSpinBox" name="kcfg_AlarmDays">
+        <property name="enabled">
+         <bool>false</bool>
+        </property>
+        <property name="prefix">
+         <string/>
+        </property>
+        <property name="maximum">
+         <number>355</number>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="groupBox_2">
+     <property name="title">
+      <string>Filter</string>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout">
+      <item>
+       <widget class="QCheckBox" name="kcfg_FilterOnCategories">
+        <property name="text">
+         <string>&amp;Filter by categories</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="Akonadi::TagSelectWidget" name="FilterCategories">
+        <property name="enabled">
+         <bool>false</bool>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <spacer name="verticalSpacer">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>18</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>Akonadi::TagSelectWidget</class>
+   <extends>QWidget</extends>
+   <header>tagselectwidget.h</header>
+  </customwidget>
+  <customwidget>
+   <class>KPluralHandlingSpinBox</class>
+   <extends>QSpinBox</extends>
+   <header>KPluralHandlingSpinBox</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>kcfg_EnableAlarm</sender>
+   <signal>toggled(bool)</signal>
+   <receiver>label</receiver>
+   <slot>setEnabled(bool)</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>97</x>
+     <y>54</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>104</x>
+     <y>75</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>kcfg_EnableAlarm</sender>
+   <signal>toggled(bool)</signal>
+   <receiver>kcfg_AlarmDays</receiver>
+   <slot>setEnabled(bool)</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>169</x>
+     <y>51</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>292</x>
+     <y>85</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>kcfg_FilterOnCategories</sender>
+   <signal>toggled(bool)</signal>
+   <receiver>FilterCategories</receiver>
+   <slot>setEnabled(bool)</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>29</x>
+     <y>151</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>81</x>
+     <y>199</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
diff --git a/resources/birthdays/settings.kcfgc b/resources/birthdays/settings.kcfgc
new file mode 100644 (file)
index 0000000..12374c3
--- /dev/null
@@ -0,0 +1,7 @@
+File=birthdaysresource.kcfg
+ClassName=Settings
+Mutators=true
+ItemAccessors=true
+SetUserTexts=true
+Singleton=true
+GlobalEnums=true
diff --git a/resources/cmake/FindXsltproc.cmake b/resources/cmake/FindXsltproc.cmake
new file mode 100644 (file)
index 0000000..45b46cf
--- /dev/null
@@ -0,0 +1,32 @@
+# Find xsltproc executable and provide a macro to generate D-Bus interfaces.
+#
+# The following variables are defined :
+# XSLTPROC_EXECUTABLE - path to the xsltproc executable
+# Xsltproc_FOUND - true if the program was found
+#
+find_program(XSLTPROC_EXECUTABLE xsltproc DOC "Path to the xsltproc executable")
+mark_as_advanced(XSLTPROC_EXECUTABLE)
+
+if(XSLTPROC_EXECUTABLE)
+  set(Xsltproc_FOUND TRUE)
+
+  # We depend on kdepimlibs, make sure it's found
+  if(NOT DEFINED KF5Akonadi_DATA_DIR)
+    find_package(KF5Akonadi REQUIRED)
+  endif()
+
+
+  # Macro to generate a D-Bus interface description from a KConfigXT file
+  macro(kcfg_generate_dbus_interface _kcfg _name)
+    add_custom_command(
+      OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${_name}.xml
+      COMMAND ${XSLTPROC_EXECUTABLE} --stringparam interfaceName ${_name}
+      ${KF5Akonadi_DATA_DIR}/kcfg2dbus.xsl
+      ${_kcfg}
+      > ${CMAKE_CURRENT_BINARY_DIR}/${_name}.xml
+      DEPENDS ${KF5Akonadi_DATA_DIR}/kcfg2dbus.xsl
+      ${_kcfg}
+      )
+  endmacro()
+endif()
+
diff --git a/resources/contacts/CMakeLists.txt b/resources/contacts/CMakeLists.txt
new file mode 100644 (file)
index 0000000..f49afb0
--- /dev/null
@@ -0,0 +1,38 @@
+
+
+add_subdirectory( wizard )
+
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_contacts_resource\")
+
+########### next target ###############
+
+set( contactsresource_SRCS
+  contactsresource.cpp
+  settingsdialog.cpp
+)
+
+ki18n_wrap_ui(contactsresource_SRCS settingsdialog.ui)
+kconfig_add_kcfg_files(contactsresource_SRCS settings.kcfgc)
+kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/contactsresource.kcfg org.kde.Akonadi.Contacts.Settings)
+qt5_add_dbus_adaptor(contactsresource_SRCS
+  ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.Contacts.Settings.xml settings.h Akonadi_Contacts_Resource::ContactsResourceSettings contactsresourcesettingsadaptor ContactsResourceSettingsAdaptor
+)
+
+ecm_qt_declare_logging_category(contactsresource_SRCS HEADER contacts_resources_debug.h IDENTIFIER CONTACTSRESOURCES_LOG CATEGORY_NAME log_resources_contacts)
+
+install( FILES contactsresource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents" )
+
+add_executable(akonadi_contacts_resource ${contactsresource_SRCS})
+
+target_link_libraries(akonadi_contacts_resource
+  KF5::AkonadiCore
+  KF5::AkonadiAgentBase
+  KF5::Contacts
+  KF5::DBusAddons
+  KF5::I18n
+  KF5::KIOWidgets
+  KF5::ConfigWidgets
+  KF5::WindowSystem
+)
+
+install(TARGETS akonadi_contacts_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
diff --git a/resources/contacts/Messages.sh b/resources/contacts/Messages.sh
new file mode 100644 (file)
index 0000000..07eba8d
--- /dev/null
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+$EXTRACTRC `find . -name \*.ui` `find . -name \*.kcfg` >> rc.cpp || exit 11
+$XGETTEXT *.cpp -o $podir/akonadi_contacts_resource.pot
diff --git a/resources/contacts/contactsresource.cpp b/resources/contacts/contactsresource.cpp
new file mode 100644 (file)
index 0000000..c986154
--- /dev/null
@@ -0,0 +1,550 @@
+/*
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "contactsresource.h"
+
+#include "settings.h"
+#include "contactsresourcesettingsadaptor.h"
+#include "settingsdialog.h"
+
+#include <QtCore/QDir>
+#include <QtCore/QDirIterator>
+#include <QtCore/QFile>
+
+#include <changerecorder.h>
+#include <collectionfetchscope.h>
+#include <entitydisplayattribute.h>
+#include <itemfetchscope.h>
+#include <kdbusconnectionpool.h>
+#include "contacts_resources_debug.h"
+
+#include <KLocalizedString>
+
+using namespace Akonadi;
+using namespace Akonadi_Contacts_Resource;
+
+ContactsResource::ContactsResource(const QString &id)
+    : ResourceBase(id),
+      mSettings(new ContactsResourceSettings(config()))
+{
+    // setup the resource
+    new ContactsResourceSettingsAdaptor(mSettings);
+    KDBusConnectionPool::threadConnection().registerObject(QStringLiteral("/Settings"),
+            mSettings, QDBusConnection::ExportAdaptors);
+
+    changeRecorder()->fetchCollection(true);
+    changeRecorder()->itemFetchScope().fetchFullPayload(true);
+    changeRecorder()->itemFetchScope().setAncestorRetrieval(ItemFetchScope::All);
+    changeRecorder()->collectionFetchScope().setAncestorRetrieval(CollectionFetchScope::All);
+
+    setHierarchicalRemoteIdentifiersEnabled(true);
+
+    mSupportedMimeTypes << KContacts::Addressee::mimeType() << KContacts::ContactGroup::mimeType() << Collection::mimeType();
+
+    if (name().startsWith(QLatin1String("akonadi_contacts_resource"))) {
+        setName(i18n("Personal Contacts"));
+    }
+
+    // Make sure we have a valid directory (XDG dirs want this very much).
+    initializeDirectory(mSettings->path());
+
+    if (mSettings->isConfigured()) {
+        synchronize();
+    }
+}
+
+ContactsResource::~ContactsResource()
+{
+    delete mSettings;
+}
+
+void ContactsResource::aboutToQuit()
+{
+}
+
+void ContactsResource::configure(WId windowId)
+{
+    QPointer<SettingsDialog> dlg = new SettingsDialog(mSettings, windowId);
+    if (dlg->exec()) {
+        mSettings->setIsConfigured(true);
+        mSettings->save();
+
+        clearCache();
+        initializeDirectory(baseDirectoryPath());
+
+        synchronize();
+
+        Q_EMIT configurationDialogAccepted();
+    } else {
+        Q_EMIT configurationDialogRejected();
+    }
+    delete dlg;
+}
+
+Collection::List ContactsResource::createCollectionsForDirectory(const QDir &parentDirectory, const Collection &parentCollection) const
+{
+    Collection::List collections;
+
+    QDir dir(parentDirectory);
+    dir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable);
+    const QFileInfoList entries = dir.entryInfoList();
+    collections.reserve(entries.count() * 2);
+
+    foreach (const QFileInfo &entry, entries) {
+        QDir subdir(entry.absoluteFilePath());
+
+        Collection collection;
+        collection.setParentCollection(parentCollection);
+        collection.setRemoteId(entry.fileName());
+        collection.setName(entry.fileName());
+        collection.setContentMimeTypes(mSupportedMimeTypes);
+        collection.setRights(supportedRights(false));
+
+        collections << collection;
+        collections << createCollectionsForDirectory(subdir, collection);
+    }
+
+    return collections;
+}
+
+void ContactsResource::retrieveCollections()
+{
+    // create the resource collection
+    Collection resourceCollection;
+    resourceCollection.setParentCollection(Collection::root());
+    resourceCollection.setRemoteId(baseDirectoryPath());
+    resourceCollection.setName(name());
+    resourceCollection.setContentMimeTypes(mSupportedMimeTypes);
+    resourceCollection.setRights(supportedRights(true));
+
+    const QDir baseDir(baseDirectoryPath());
+
+    Collection::List collections = createCollectionsForDirectory(baseDir, resourceCollection);
+    collections.append(resourceCollection);
+
+    collectionsRetrieved(collections);
+}
+
+void ContactsResource::retrieveItems(const Akonadi::Collection &collection)
+{
+    QDir directory(directoryForCollection(collection));
+    if (!directory.exists()) {
+        cancelTask(i18n("Directory '%1' does not exists", collection.remoteId()));
+        return;
+    }
+
+    directory.setFilter(QDir::Files | QDir::Readable);
+
+    Item::List items;
+
+    const QFileInfoList entries = directory.entryInfoList();
+
+    foreach (const QFileInfo &entry, entries) {
+        if (entry.fileName() == QLatin1String("WARNING_README.txt")) {
+            continue;
+        }
+
+        Item item;
+        item.setRemoteId(entry.fileName());
+
+        if (entry.fileName().endsWith(QLatin1String(".vcf"))) {
+            item.setMimeType(KContacts::Addressee::mimeType());
+        } else if (entry.fileName().endsWith(QLatin1String(".ctg"))) {
+            item.setMimeType(KContacts::ContactGroup::mimeType());
+        } else {
+            cancelTask(i18n("Found file of unknown format: '%1'", entry.absoluteFilePath()));
+            return;
+        }
+
+        items.append(item);
+    }
+
+    itemsRetrieved(items);
+}
+
+bool ContactsResource::retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &)
+{
+    const QString filePath = directoryForCollection(item.parentCollection()) + QDir::separator() + item.remoteId();
+
+    Item newItem(item);
+
+    QFile file(filePath);
+    if (!file.open(QIODevice::ReadOnly)) {
+        cancelTask(i18n("Unable to open file '%1'", filePath));
+        return false;
+    }
+
+    if (filePath.endsWith(QLatin1String(".vcf"))) {
+        KContacts::VCardConverter converter;
+
+        const QByteArray content = file.readAll();
+        const KContacts::Addressee contact = converter.parseVCard(content);
+        if (contact.isEmpty()) {
+            cancelTask(i18n("Found invalid contact in file '%1'", filePath));
+            return false;
+        }
+
+        newItem.setPayload<KContacts::Addressee>(contact);
+    } else if (filePath.endsWith(QLatin1String(".ctg"))) {
+        KContacts::ContactGroup group;
+        QString errorMessage;
+
+        if (!KContacts::ContactGroupTool::convertFromXml(&file, group, &errorMessage)) {
+            cancelTask(i18n("Found invalid contact group in file '%1': %2", filePath, errorMessage));
+            return false;
+        }
+
+        newItem.setPayload<KContacts::ContactGroup>(group);
+    } else {
+        cancelTask(i18n("Found file of unknown format: '%1'", filePath));
+        return false;
+    }
+
+    file.close();
+
+    itemRetrieved(newItem);
+
+    return true;
+}
+
+void ContactsResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection)
+{
+    if (mSettings->readOnly()) {
+        cancelTask(i18n("Trying to write to a read-only directory: '%1'", collection.remoteId()));
+        return;
+    }
+
+    const QString directoryPath = directoryForCollection(collection);
+
+    Item newItem(item);
+
+    if (item.hasPayload<KContacts::Addressee>()) {
+        const KContacts::Addressee contact = item.payload<KContacts::Addressee>();
+
+        const QString fileName = directoryPath + QDir::separator() + contact.uid() + QLatin1String(".vcf");
+
+        KContacts::VCardConverter converter;
+        const QByteArray content = converter.createVCard(contact);
+
+        QFile file(fileName);
+        if (!file.open(QIODevice::WriteOnly)) {
+            cancelTask(i18n("Unable to write to file '%1': %2", fileName, file.errorString()));
+            return;
+        }
+
+        file.write(content);
+        file.close();
+
+        newItem.setRemoteId(contact.uid() + QLatin1String(".vcf"));
+
+    } else if (item.hasPayload<KContacts::ContactGroup>()) {
+        const KContacts::ContactGroup group = item.payload<KContacts::ContactGroup>();
+
+        const QString fileName = directoryPath + QDir::separator() + group.id() + QLatin1String(".ctg");
+
+        QFile file(fileName);
+        if (!file.open(QIODevice::WriteOnly)) {
+            cancelTask(i18n("Unable to write to file '%1': %2", fileName, file.errorString()));
+            return;
+        }
+
+        KContacts::ContactGroupTool::convertToXml(group, &file);
+
+        file.close();
+
+        newItem.setRemoteId(group.id() + QLatin1String(".ctg"));
+
+    } else {
+        qCWarning(CONTACTSRESOURCES_LOG) << "got item without (usable) payload, ignoring it";
+    }
+
+    changeCommitted(newItem);
+}
+
+void ContactsResource::itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &)
+{
+    if (mSettings->readOnly()) {
+        cancelTask(i18n("Trying to write to a read-only file: '%1'", item.remoteId()));
+        return;
+    }
+
+    Item newItem(item);
+
+    const QString fileName = directoryForCollection(item.parentCollection()) + QDir::separator() + item.remoteId();
+
+    if (item.hasPayload<KContacts::Addressee>()) {
+        const KContacts::Addressee contact = item.payload<KContacts::Addressee>();
+
+        KContacts::VCardConverter converter;
+        const QByteArray content = converter.createVCard(contact);
+
+        QFile file(fileName);
+        if (!file.open(QIODevice::WriteOnly)) {
+            cancelTask(i18n("Unable to write to file '%1': %2", fileName, file.errorString()));
+            return;
+        }
+        file.write(content);
+        file.close();
+
+        newItem.setRemoteId(item.remoteId());
+
+    } else if (item.hasPayload<KContacts::ContactGroup>()) {
+        const KContacts::ContactGroup group = item.payload<KContacts::ContactGroup>();
+
+        QFile file(fileName);
+        if (!file.open(QIODevice::WriteOnly)) {
+            cancelTask(i18n("Unable to write to file '%1': %2", fileName, file.errorString()));
+            return;
+        }
+
+        KContacts::ContactGroupTool::convertToXml(group, &file);
+
+        file.close();
+
+        newItem.setRemoteId(item.remoteId());
+
+    } else {
+        cancelTask(i18n("Received item with unknown payload %1", item.mimeType()));
+        return;
+    }
+
+    changeCommitted(newItem);
+}
+
+void ContactsResource::itemRemoved(const Akonadi::Item &item)
+{
+    if (mSettings->readOnly()) {
+        cancelTask(i18n("Trying to write to a read-only file: '%1'", item.remoteId()));
+        return;
+    }
+
+    // If the parent collection has no valid remote id, the parent
+    // collection will be removed in a second, so stop here and remove
+    // all items in collectionRemoved().
+    if (item.parentCollection().remoteId().isEmpty()) {
+        changeProcessed();
+        return;
+    }
+
+    const QString fileName = directoryForCollection(item.parentCollection()) + QDir::separator() + item.remoteId();
+
+    if (!QFile::remove(fileName)) {
+        cancelTask(i18n("Unable to remove file '%1'", fileName));
+        return;
+    }
+
+    changeProcessed();
+}
+
+void ContactsResource::collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent)
+{
+    if (mSettings->readOnly()) {
+        cancelTask(i18n("Trying to write to a read-only directory: '%1'", parent.remoteId()));
+        return;
+    }
+
+    const QString dirName = directoryForCollection(parent) + QDir::separator() + collection.name();
+
+    if (!QDir::root().mkpath(dirName)) {
+        cancelTask(i18n("Unable to create folder '%1'.", dirName));
+        return;
+    }
+
+    initializeDirectory(dirName);
+
+    Collection newCollection(collection);
+    newCollection.setRemoteId(collection.name());
+    changeCommitted(newCollection);
+}
+
+void ContactsResource::collectionChanged(const Akonadi::Collection &collection)
+{
+    if (mSettings->readOnly()) {
+        cancelTask(i18n("Trying to write to a read-only directory: '%1'", collection.remoteId()));
+        return;
+    }
+
+    if (collection.parentCollection() == Collection::root()) {
+        if (collection.name() != name()) {
+            setName(collection.name());
+        }
+        changeProcessed();
+        return;
+    }
+
+    if (collection.remoteId() == collection.name()) {
+        changeProcessed();
+        return;
+    }
+
+    const QString dirName = directoryForCollection(collection);
+
+    QFileInfo oldDirectory(dirName);
+    if (!QDir::root().rename(dirName, oldDirectory.absolutePath() + QDir::separator() + collection.name())) {
+        cancelTask(i18n("Unable to rename folder '%1'.", collection.name()));
+        return;
+    }
+
+    Collection newCollection(collection);
+    newCollection.setRemoteId(collection.name());
+    changeCommitted(newCollection);
+}
+
+/**
+ * Removes a @p directory recursively.
+ */
+static bool removeDirectory(const QDir &directory)
+{
+    const QFileInfoList infoList =
+        directory.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
+
+    foreach (const QFileInfo &info, infoList) {
+        if (info.isDir()) {
+            if (!removeDirectory(QDir(info.absoluteFilePath()))) {
+                return false;
+            }
+        } else {
+            if (!QFile::remove(info.filePath())) {
+                return false;
+            }
+        }
+    }
+
+    if (!QDir::root().rmdir(directory.absolutePath())) {
+        return false;
+    }
+
+    return true;
+}
+
+void ContactsResource::collectionRemoved(const Akonadi::Collection &collection)
+{
+    if (mSettings->readOnly()) {
+        cancelTask(i18n("Trying to write to a read-only directory: '%1'", collection.remoteId()));
+        return;
+    }
+
+    if (!removeDirectory(directoryForCollection(collection))) {
+        cancelTask(i18n("Unable to delete folder '%1'.", collection.name()));
+        return;
+    }
+
+    changeProcessed();
+}
+
+void ContactsResource::itemMoved(const Akonadi::Item &item, const Akonadi::Collection &collectionSource,
+                                 const Akonadi::Collection &collectionDestination)
+{
+    const QString sourceFileName = directoryForCollection(collectionSource) + QDir::separator() + item.remoteId();
+    const QString targetFileName = directoryForCollection(collectionDestination) + QDir::separator() + item.remoteId();
+
+    if (QFile::rename(sourceFileName, targetFileName)) {
+        changeProcessed();
+    } else {
+        cancelTask(i18n("Unable to move file '%1' to '%2', '%2' already exists.", sourceFileName, targetFileName));
+    }
+}
+
+void ContactsResource::collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &collectionSource,
+                                       const Akonadi::Collection &collectionDestination)
+{
+    const QString sourceDirectoryName = directoryForCollection(collectionSource) + QDir::separator() + collection.remoteId();
+    const QString targetDirectoryName = directoryForCollection(collectionDestination) + QDir::separator() + collection.remoteId();
+
+    if (QFile::rename(sourceDirectoryName, targetDirectoryName)) {
+        changeProcessed();
+    } else {
+        cancelTask(i18n("Unable to move directory '%1' to '%2', '%2' already exists.", sourceDirectoryName, targetDirectoryName));
+    }
+}
+
+QString ContactsResource::baseDirectoryPath() const
+{
+    return mSettings->path();
+}
+
+void ContactsResource::initializeDirectory(const QString &path) const
+{
+    QDir dir(path);
+
+    // if folder does not exists, create it
+    if (!dir.exists()) {
+        QDir::root().mkpath(dir.absolutePath());
+    }
+
+    // check whether warning file is in place...
+    QFile file(dir.absolutePath() + QDir::separator() + QLatin1String("WARNING_README.txt"));
+    if (!file.exists()) {
+        // ... if not, create it
+        file.open(QIODevice::WriteOnly);
+        file.write("Important Warning!!!\n\n"
+                   "Don't create or copy vCards inside this folder manually, they are managed by the Akonadi framework!\n");
+        file.close();
+    }
+}
+
+Collection::Rights ContactsResource::supportedRights(bool isResourceCollection) const
+{
+    Collection::Rights rights = Collection::ReadOnly;
+
+    if (!mSettings->readOnly()) {
+        rights |= Collection::CanChangeItem;
+        rights |= Collection::CanCreateItem;
+        rights |= Collection::CanDeleteItem;
+        rights |= Collection::CanCreateCollection;
+        rights |= Collection::CanChangeCollection;
+
+        if (!isResourceCollection) {
+            rights |= Collection::CanDeleteCollection;
+        }
+    }
+
+    return rights;
+}
+
+QString ContactsResource::directoryForCollection(const Collection &collection) const
+{
+    if (collection.remoteId().isEmpty()) {
+        qCWarning(CONTACTSRESOURCES_LOG) << "Got incomplete ancestor chain:" << collection;
+        return QString();
+    }
+
+    if (collection.parentCollection() == Collection::root()) {
+        if (collection.remoteId() != baseDirectoryPath())
+            qCWarning(CONTACTSRESOURCES_LOG) << "RID mismatch, is " << collection.remoteId()
+                                             << " expected " << baseDirectoryPath();
+        return collection.remoteId();
+    }
+
+    const QString parentDirectory = directoryForCollection(collection.parentCollection());
+    if (parentDirectory.isNull()) { // invalid, != isEmpty() here!
+        return QString();
+    }
+
+    QString directory = parentDirectory;
+    if (!directory.endsWith(QLatin1Char('/'))) {
+        directory += QDir::separator() + collection.remoteId();
+    } else {
+        directory += collection.remoteId();
+    }
+
+    return directory;
+}
+
+AKONADI_RESOURCE_MAIN(ContactsResource)
diff --git a/resources/contacts/contactsresource.desktop b/resources/contacts/contactsresource.desktop
new file mode 100644 (file)
index 0000000..703ce03
--- /dev/null
@@ -0,0 +1,101 @@
+[Desktop Entry]
+Name=Personal Contacts
+Name[ast]=Contautos personales
+Name[bg]=Лични контакти
+Name[bs]=Lični kontakti
+Name[ca]=Contactes personals
+Name[ca@valencia]=Contactes personals
+Name[cs]=Osobní kontakty
+Name[da]=Personlige kontakter
+Name[de]=Persönliche Kontakte
+Name[el]=Προσωπικές επαφές
+Name[en_GB]=Personal Contacts
+Name[es]=Contactos personales
+Name[et]=Isiklikud kontaktid
+Name[fi]=Omat yhteystiedot
+Name[fr]=Contacts personnels
+Name[ga]=Teagmhálacha Pearsanta
+Name[gl]=Contactos Persoais
+Name[hu]=Személyes névjegyek
+Name[ia]=Contactos personal
+Name[it]=Contatti personali
+Name[ja]=個人の連絡先
+Name[kk]=Дербес контакттар
+Name[km]=ទំនាក់ទំនង​ផ្ទាល់ខ្លួន
+Name[ko]=개인 연락처
+Name[lt]=Asmeniniai kontaktai
+Name[lv]=Personīgie kontakti
+Name[nb]=Personlige kontakter
+Name[nds]=Persöönlich Kontakten
+Name[nl]=Persoonlijke contacten
+Name[nn]=Personlege kontaktar
+Name[pa]=ਨਿੱਜੀ ਸੰਪਰਕ
+Name[pl]=Kontakty osobiste
+Name[pt]=Contactos Pessoais
+Name[pt_BR]=Contatos pessoais
+Name[ro]=Contacte personale
+Name[ru]=Личные контакты
+Name[sk]=Osobné kontakty
+Name[sl]=Osebni stiki
+Name[sr]=Лични контакти
+Name[sr@ijekavian]=Лични контакти
+Name[sr@ijekavianlatin]=Lični kontakti
+Name[sr@latin]=Lični kontakti
+Name[sv]=Personliga kontakter
+Name[tr]=Kişisel Bağlantılar
+Name[ug]=شەخسىي ئالاقەداشلار
+Name[uk]=Особисті контакти
+Name[x-test]=xxPersonal Contactsxx
+Name[zh_CN]=个人联系人
+Name[zh_TW]=個人聯絡人
+Comment=The address book with personal contacts
+Comment[ast]=La llibreta de direiciones con contautos personales
+Comment[bs]=Imenik sa ličnim kontaktima
+Comment[ca]=La llibreta d'adreces amb contactes personals
+Comment[ca@valencia]=La llibreta d'adreces amb contactes personals
+Comment[da]=Adressebog med personlige kontakter
+Comment[de]=Das Adressbuch mit persönlichen Kontakten
+Comment[el]=Το βιβλίο διευθύνσεων με τις προσωπικές επαφές
+Comment[en_GB]=The address book with personal contacts
+Comment[es]=La libreta de direcciones con contactos personales
+Comment[et]=Isiklikke kontakte sisaldav aadressiraamat
+Comment[fi]=Henkilökohtaisten yhteystietojen osoitekirja
+Comment[fr]=Le carnet d'adresses avec vos contacts personnels
+Comment[gl]=O caderno de enderezos cos contactos persoais
+Comment[hu]=A személyes névjegyeket tartalmazó címjegyzék
+Comment[ia]=Le adressario con contactos personal
+Comment[it]=La rubrica con i contatti personali
+Comment[ja]=個人の連絡先を含むアドレス帳
+Comment[kk]=Дербес контакттары жазатын адрестік кітапша
+Comment[km]=សៀវភៅ​អាសយដ្ឋាន​ដែល​មាន​ទំនាក់ទំនង​ផ្ទាល់ខ្លួន
+Comment[ko]=개인 연락처가 있는 주소록
+Comment[lt]=Adresų knygelė su asmeniniais kontaktais
+Comment[lv]=Personīgo kontaktu adrešu grāmata
+Comment[nb]=Adresseboka med personlige kontakter.
+Comment[nds]=Dat Adressbook mit persöönlich Kontakten
+Comment[nl]=Het adresboek met persoonlijke contacten
+Comment[pa]=ਨਿੱਜੀ ਸੰਪਰਕਾਂ ਨਾਲ ਐਡਰੈੱਸ ਬੁੱਕ
+Comment[pl]=Książka adresowa z osobistymi kontaktami
+Comment[pt]=O livro de endereços com os contactos pessoais
+Comment[pt_BR]=O livro de endereços com contatos pessoais
+Comment[ro]=Cartea de adrese cu contacte personale
+Comment[ru]=Адресная книга с личными контактами
+Comment[sk]=Adresár s osobnými kontaktmi
+Comment[sl]=Imenik z osebnimi stiki
+Comment[sr]=Адресар са личним контактима
+Comment[sr@ijekavian]=Адресар са личним контактима
+Comment[sr@ijekavianlatin]=Adresar sa ličnim kontaktima
+Comment[sr@latin]=Adresar sa ličnim kontaktima
+Comment[sv]=Adressboken med personliga kontakter
+Comment[tr]=Kişisel bağlantıları içeren adres defteri
+Comment[uk]=Адресна книга з особистими записами контактів
+Comment[x-test]=xxThe address book with personal contactsxx
+Comment[zh_CN]=个人联系地址簿
+Comment[zh_TW]=個人聯絡人的通訊錄
+Type=AkonadiResource
+Exec=akonadi_contacts_resource
+Icon=text-directory
+
+X-Akonadi-MimeTypes=text/directory,application/x-vnd.kde.contactgroup
+X-Akonadi-Capabilities=Resource
+X-Akonadi-Identifier=akonadi_contacts_resource
diff --git a/resources/contacts/contactsresource.h b/resources/contacts/contactsresource.h
new file mode 100644 (file)
index 0000000..9a092fe
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef CONTACTSRESOURCE_H
+#define CONTACTSRESOURCE_H
+
+#include <resourcebase.h>
+
+#include <kcontacts/addressee.h>
+#include <kcontacts/contactgroup.h>
+#include <kcontacts/contactgrouptool.h>
+#include <kcontacts/vcardconverter.h>
+
+namespace Akonadi_Contacts_Resource
+{
+class ContactsResourceSettings;
+}
+class QDir;
+
+class ContactsResource : public Akonadi::ResourceBase, public Akonadi::AgentBase::ObserverV2
+{
+    Q_OBJECT
+
+public:
+    explicit ContactsResource(const QString &id);
+    ~ContactsResource();
+
+public Q_SLOTS:
+    void configure(WId windowId) Q_DECL_OVERRIDE;
+    void aboutToQuit() Q_DECL_OVERRIDE;
+
+protected Q_SLOTS:
+    void retrieveCollections() Q_DECL_OVERRIDE;
+    void retrieveItems(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    bool retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+
+protected:
+    void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    void itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+    void itemRemoved(const Akonadi::Item &item) Q_DECL_OVERRIDE;
+
+    void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) Q_DECL_OVERRIDE;
+    void collectionChanged(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    // do not hide the other variant, use implementation from base class
+    // which just forwards to the one above
+    using Akonadi::AgentBase::ObserverV2::collectionChanged;
+    void collectionRemoved(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+
+    void itemMoved(const Akonadi::Item &item, const Akonadi::Collection &collectionSource,
+                   const Akonadi::Collection &collectionDestination) Q_DECL_OVERRIDE;
+    void collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &collectionSource,
+                         const Akonadi::Collection &collectionDestination) Q_DECL_OVERRIDE;
+
+private:
+    Akonadi::Collection::List createCollectionsForDirectory(const QDir &parentDirectory,
+            const Akonadi::Collection &parentCollection) const;
+    QString baseDirectoryPath() const;
+    void initializeDirectory(const QString &path) const;
+    Akonadi::Collection::Rights supportedRights(bool isResourceCollection) const;
+    QString directoryForCollection(const Akonadi::Collection &collection) const;
+
+private:
+    QStringList mSupportedMimeTypes;
+    Akonadi_Contacts_Resource::ContactsResourceSettings *mSettings;
+};
+
+#endif
diff --git a/resources/contacts/contactsresource.kcfg b/resources/contacts/contactsresource.kcfg
new file mode 100644 (file)
index 0000000..d580b90
--- /dev/null
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:kcfg="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
+      http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
+  <kcfgfile arg="true"/>
+  <group name="General">
+    <entry name="Path" type="Path">
+      <label>Path to contacts directory</label>
+      <default>$HOME/.local/share/contacts/</default>
+    </entry>
+    <entry name="ReadOnly" type="Bool">
+      <label>Do not change the actual backend data.</label>
+      <default>false</default>
+    </entry>
+    <entry name="IsConfigured" type="Bool">
+      <default>false</default>
+    </entry>
+  </group>
+</kcfg>
diff --git a/resources/contacts/settings.kcfgc b/resources/contacts/settings.kcfgc
new file mode 100644 (file)
index 0000000..a019315
--- /dev/null
@@ -0,0 +1,8 @@
+File=contactsresource.kcfg
+ClassName=ContactsResourceSettings
+Mutators=true
+ItemAccessors=true
+SetUserTexts=true
+Singleton=false
+GlobalEnums=true
+NameSpace=Akonadi_Contacts_Resource
diff --git a/resources/contacts/settingsdialog.cpp b/resources/contacts/settingsdialog.cpp
new file mode 100644 (file)
index 0000000..2bc2a08
--- /dev/null
@@ -0,0 +1,117 @@
+/*
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "settingsdialog.h"
+#include "settings.h"
+
+#include <KConfigDialogManager>
+#include <KWindowSystem>
+
+#include <QTimer>
+#include <KSharedConfig>
+#include <QUrl>
+#include <QDialogButtonBox>
+#include <KConfigGroup>
+#include <QPushButton>
+#include <QVBoxLayout>
+using namespace Akonadi;
+using namespace Akonadi_Contacts_Resource;
+
+SettingsDialog::SettingsDialog(ContactsResourceSettings *settings, WId windowId)
+    : QDialog(),
+      mSettings(settings)
+{
+    QWidget *mainWidget = new QWidget(this);
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    setLayout(mainLayout);
+    mainLayout->addWidget(mainWidget);
+    ui.setupUi(mainWidget);
+    setWindowIcon(QIcon::fromTheme(QStringLiteral("text-directory")));
+    ui.kcfg_Path->setMode(KFile::LocalOnly | KFile::Directory);
+
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    mOkButton = buttonBox->button(QDialogButtonBox::Ok);
+    mOkButton->setDefault(true);
+    mOkButton->setShortcut(Qt::CTRL | Qt::Key_Return);
+    connect(buttonBox, &QDialogButtonBox::accepted, this, &SettingsDialog::accept);
+    connect(buttonBox, &QDialogButtonBox::rejected, this, &SettingsDialog::reject);
+    mainLayout->addWidget(buttonBox);
+
+    if (windowId) {
+        KWindowSystem::setMainWindow(this, windowId);
+    }
+
+    connect(mOkButton, &QPushButton::clicked, this, &SettingsDialog::save);
+
+    connect(ui.kcfg_Path, &KUrlRequester::textChanged, this, &SettingsDialog::validate);
+    connect(ui.kcfg_ReadOnly, &QCheckBox::toggled, this, &SettingsDialog::validate);
+
+    QTimer::singleShot(0, this, &SettingsDialog::validate);
+
+    ui.kcfg_Path->setUrl(QUrl::fromLocalFile(mSettings->path()));
+    mManager = new KConfigDialogManager(this, mSettings);
+    mManager->updateWidgets();
+    readConfig();
+}
+
+SettingsDialog::~SettingsDialog()
+{
+    writeConfig();
+}
+
+void SettingsDialog::save()
+{
+    mManager->updateSettings();
+    mSettings->setPath(ui.kcfg_Path->url().toLocalFile());
+    mSettings->save();
+}
+
+void SettingsDialog::validate()
+{
+    const QUrl currentUrl = ui.kcfg_Path->url();
+    if (currentUrl.isEmpty()) {
+        mOkButton->setEnabled(false);
+        return;
+    }
+
+    const QFileInfo file(currentUrl.toLocalFile());
+    if (file.exists() && !file.isWritable()) {
+        ui.kcfg_ReadOnly->setEnabled(false);
+        ui.kcfg_ReadOnly->setChecked(true);
+    } else {
+        ui.kcfg_ReadOnly->setEnabled(true);
+    }
+    mOkButton->setEnabled(true);
+}
+
+void SettingsDialog::readConfig()
+{
+    KConfigGroup group(KSharedConfig::openConfig(), "SettingsDialog");
+    const QSize size = group.readEntry("Size", QSize(600, 400));
+    if (size.isValid()) {
+        resize(size);
+    }
+}
+
+void SettingsDialog::writeConfig()
+{
+    KConfigGroup group(KSharedConfig::openConfig(), "SettingsDialog");
+    group.writeEntry("Size", size());
+    group.sync();
+}
diff --git a/resources/contacts/settingsdialog.h b/resources/contacts/settingsdialog.h
new file mode 100644 (file)
index 0000000..799d4ca
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef AKONADI_SETTINGSDIALOG_H
+#define AKONADI_SETTINGSDIALOG_H
+
+#include "ui_settingsdialog.h"
+
+#include <QDialog>
+class QPushButton;
+namespace Akonadi_Contacts_Resource
+{
+class ContactsResourceSettings;
+}
+class KConfigDialogManager;
+
+namespace Akonadi
+{
+
+class SettingsDialog : public QDialog
+{
+    Q_OBJECT
+public:
+    explicit SettingsDialog(Akonadi_Contacts_Resource::ContactsResourceSettings *settings, WId windowId);
+    ~SettingsDialog();
+
+private Q_SLOTS:
+    void save();
+    void validate();
+
+private:
+    void readConfig();
+    void writeConfig();
+    Ui::SettingsDialog ui;
+    KConfigDialogManager *mManager;
+    Akonadi_Contacts_Resource::ContactsResourceSettings *mSettings;
+    QPushButton *mOkButton;
+};
+
+}
+
+#endif
diff --git a/resources/contacts/settingsdialog.ui b/resources/contacts/settingsdialog.ui
new file mode 100644 (file)
index 0000000..3587390
--- /dev/null
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SettingsDialog</class>
+ <widget class="QWidget" name="SettingsDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>547</width>
+    <height>386</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QGroupBox" name="groupBox_2">
+     <property name="title">
+      <string>Directory Name</string>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout_3">
+      <item>
+       <layout class="QHBoxLayout" name="horizontalLayout">
+        <item>
+         <widget class="QLabel" name="label">
+          <property name="text">
+           <string>Di&amp;rectory:</string>
+          </property>
+          <property name="buddy">
+           <cstring>kcfg_Path</cstring>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="KUrlRequester" name="kcfg_Path"/>
+        </item>
+       </layout>
+      </item>
+      <item>
+       <widget class="QLabel" name="label_3">
+        <property name="text">
+         <string>Select the directory whose contents should be represented by this resource. If the directory does not exist, it will be created.</string>
+        </property>
+        <property name="wordWrap">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="groupBox">
+     <property name="title">
+      <string>Access Rights</string>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout_2">
+      <item>
+       <widget class="QCheckBox" name="kcfg_ReadOnly">
+        <property name="text">
+         <string comment="if the access is limited to read-only">Read only</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QLabel" name="label_2">
+        <property name="text">
+         <string>If read-only mode is enabled, no changes will be written to the directory selected above. Read-only mode will be automatically enabled if you do not have write access to the directory.</string>
+        </property>
+        <property name="wordWrap">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KUrlRequester</class>
+   <extends>QFrame</extends>
+   <header>kurlrequester.h</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/resources/contacts/wizard/CMakeLists.txt b/resources/contacts/wizard/CMakeLists.txt
new file mode 100644 (file)
index 0000000..731c3f1
--- /dev/null
@@ -0,0 +1,5 @@
+
+set(CONTACTS_DIRECTORY_DEFAULT_PATH "$HOME/.local/share/contacts/")
+
+configure_file(contactswizard.es.cmake ${CMAKE_CURRENT_BINARY_DIR}/contactswizard.es)
+install ( FILES contactswizard.desktop ${CMAKE_CURRENT_BINARY_DIR}/contactswizard.es contactswizard.ui DESTINATION ${KDE_INSTALL_DATADIR}/akonadi/accountwizard/contacts/ )
diff --git a/resources/contacts/wizard/Messages.sh b/resources/contacts/wizard/Messages.sh
new file mode 100644 (file)
index 0000000..7ce2c32
--- /dev/null
@@ -0,0 +1,6 @@
+#! /usr/bin/env bash
+$EXTRACTRC *.ui >> rc.cpp
+$XGETTEXT *.cpp -o $podir/accountwizard_contacts.pot
+$XGETTEXT -kqsTr *.es.cmake -j -o $podir/accountwizard_contacts.pot
+rm rc.cpp
+
diff --git a/resources/contacts/wizard/contactswizard.desktop b/resources/contacts/wizard/contactswizard.desktop
new file mode 100644 (file)
index 0000000..f450555
--- /dev/null
@@ -0,0 +1,102 @@
+[Desktop Entry]
+Name=Personal Contacts
+Name[ast]=Contautos personales
+Name[bg]=Лични контакти
+Name[bs]=Lični kontakti
+Name[ca]=Contactes personals
+Name[ca@valencia]=Contactes personals
+Name[cs]=Osobní kontakty
+Name[da]=Personlige kontakter
+Name[de]=Persönliche Kontakte
+Name[el]=Προσωπικές επαφές
+Name[en_GB]=Personal Contacts
+Name[es]=Contactos personales
+Name[et]=Isiklikud kontaktid
+Name[fi]=Omat yhteystiedot
+Name[fr]=Contacts personnels
+Name[ga]=Teagmhálacha Pearsanta
+Name[gl]=Contactos Persoais
+Name[hu]=Személyes névjegyek
+Name[ia]=Contactos personal
+Name[it]=Contatti personali
+Name[ja]=個人の連絡先
+Name[kk]=Дербес контакттар
+Name[km]=ទំនាក់ទំនង​ផ្ទាល់ខ្លួន
+Name[ko]=개인 연락처
+Name[lt]=Asmeniniai kontaktai
+Name[lv]=Personīgie kontakti
+Name[nb]=Personlige kontakter
+Name[nds]=Persöönlich Kontakten
+Name[nl]=Persoonlijke contacten
+Name[nn]=Personlege kontaktar
+Name[pa]=ਨਿੱਜੀ ਸੰਪਰਕ
+Name[pl]=Kontakty osobiste
+Name[pt]=Contactos Pessoais
+Name[pt_BR]=Contatos pessoais
+Name[ro]=Contacte personale
+Name[ru]=Личные контакты
+Name[sk]=Osobné kontakty
+Name[sl]=Osebni stiki
+Name[sr]=Лични контакти
+Name[sr@ijekavian]=Лични контакти
+Name[sr@ijekavianlatin]=Lični kontakti
+Name[sr@latin]=Lični kontakti
+Name[sv]=Personliga kontakter
+Name[tr]=Kişisel Bağlantılar
+Name[ug]=شەخسىي ئالاقەداشلار
+Name[uk]=Особисті контакти
+Name[x-test]=xxPersonal Contactsxx
+Name[zh_CN]=个人联系人
+Name[zh_TW]=個人聯絡人
+Comment=The address book with personal contacts
+Comment[ast]=La llibreta de direiciones con contautos personales
+Comment[bs]=Imenik sa ličnim kontaktima
+Comment[ca]=La llibreta d'adreces amb contactes personals
+Comment[ca@valencia]=La llibreta d'adreces amb contactes personals
+Comment[da]=Adressebog med personlige kontakter
+Comment[de]=Das Adressbuch mit persönlichen Kontakten
+Comment[el]=Το βιβλίο διευθύνσεων με τις προσωπικές επαφές
+Comment[en_GB]=The address book with personal contacts
+Comment[es]=La libreta de direcciones con contactos personales
+Comment[et]=Isiklikke kontakte sisaldav aadressiraamat
+Comment[fi]=Henkilökohtaisten yhteystietojen osoitekirja
+Comment[fr]=Le carnet d'adresses avec vos contacts personnels
+Comment[gl]=O caderno de enderezos cos contactos persoais
+Comment[hu]=A személyes névjegyeket tartalmazó címjegyzék
+Comment[ia]=Le adressario con contactos personal
+Comment[it]=La rubrica con i contatti personali
+Comment[ja]=個人の連絡先を含むアドレス帳
+Comment[kk]=Дербес контакттары жазатын адрестік кітапша
+Comment[km]=សៀវភៅ​អាសយដ្ឋាន​ដែល​មាន​ទំនាក់ទំនង​ផ្ទាល់ខ្លួន
+Comment[ko]=개인 연락처가 있는 주소록
+Comment[lt]=Adresų knygelė su asmeniniais kontaktais
+Comment[lv]=Personīgo kontaktu adrešu grāmata
+Comment[nb]=Adresseboka med personlige kontakter.
+Comment[nds]=Dat Adressbook mit persöönlich Kontakten
+Comment[nl]=Het adresboek met persoonlijke contacten
+Comment[pa]=ਨਿੱਜੀ ਸੰਪਰਕਾਂ ਨਾਲ ਐਡਰੈੱਸ ਬੁੱਕ
+Comment[pl]=Książka adresowa z osobistymi kontaktami
+Comment[pt]=O livro de endereços com os contactos pessoais
+Comment[pt_BR]=O livro de endereços com contatos pessoais
+Comment[ro]=Cartea de adrese cu contacte personale
+Comment[ru]=Адресная книга с личными контактами
+Comment[sk]=Adresár s osobnými kontaktmi
+Comment[sl]=Imenik z osebnimi stiki
+Comment[sr]=Адресар са личним контактима
+Comment[sr@ijekavian]=Адресар са личним контактима
+Comment[sr@ijekavianlatin]=Adresar sa ličnim kontaktima
+Comment[sr@latin]=Adresar sa ličnim kontaktima
+Comment[sv]=Adressboken med personliga kontakter
+Comment[tr]=Kişisel bağlantıları içeren adres defteri
+Comment[uk]=Адресна книга з особистими записами контактів
+Comment[x-test]=xxThe address book with personal contactsxx
+Comment[zh_CN]=个人联系地址簿
+Comment[zh_TW]=個人聯絡人的通訊錄
+Icon=text-directory
+
+[Wizard]
+Type=text/directory,application/x-vnd.kde.contactgroup
+Script=contactswizard.es
+
+[Translate]
+Filename=accountwizard_contacts
diff --git a/resources/contacts/wizard/contactswizard.es.cmake b/resources/contacts/wizard/contactswizard.es.cmake
new file mode 100644 (file)
index 0000000..3121873
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+var page = Dialog.addPage( "contactswizard.ui", qsTr("Settings") );
+
+page.widget().lineEdit.text = "${CONTACTS_DIRECTORY_DEFAULT_PATH}";
+
+function validateInput()
+{
+  if ( page.widget().lineEdit.text == "" ) {
+    page.setValid( false );
+  } else {
+    page.setValid( true );
+  }
+}
+
+function setup()
+{
+  var contactsResource = SetupManager.createResource( "akonadi_contacts_resource" );
+  contactsResource.setOption( "Path", page.widget().lineEdit.text );
+  contactsResource.setOption( "IsConfigured", "true" );
+  contactsResource.setName( qsTr("Local Contacts") );
+  SetupManager.execute();
+}
+
+page.widget().lineEdit.textChanged.connect( validateInput );
+page.pageLeftNext.connect( setup );
+validateInput();
diff --git a/resources/contacts/wizard/contactswizard.ui b/resources/contacts/wizard/contactswizard.ui
new file mode 100644 (file)
index 0000000..b4fd4a2
--- /dev/null
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>contactsWizard</class>
+ <widget class="QWidget" name="contactsWizard">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>300</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QLabel" name="label">
+       <property name="text">
+        <string>Filename:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="KLineEdit" name="lineEdit"/>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <spacer name="verticalSpacer_2">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>138</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KLineEdit</class>
+   <extends>QLineEdit</extends>
+   <header>klineedit.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/resources/dav/CMakeLists.txt b/resources/dav/CMakeLists.txt
new file mode 100644 (file)
index 0000000..7590124
--- /dev/null
@@ -0,0 +1 @@
+add_subdirectory( resource )
diff --git a/resources/dav/COPYING b/resources/dav/COPYING
new file mode 100644 (file)
index 0000000..dcfa4c2
--- /dev/null
@@ -0,0 +1,340 @@
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+                       59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                   GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                           NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/resources/dav/README b/resources/dav/README
new file mode 100644 (file)
index 0000000..8809f35
--- /dev/null
@@ -0,0 +1,50 @@
+==== What's this ? ====
+
+This is an Akonadi resource to access DAV-enabled PIM storages.
+
+Calendars and todos are supported, using either GroupDAV
+or CalDAV, and contacts are supported using GroupDAV or
+CardDAV.
+
+
+==== Usage ====
+
+It should be pretty straightforward. The URL to use should be, if possible,
+the principals URL for your server (CalDAV / CardDAV) or the parent
+collection of your calendars to allow discovery.
+
+
+==== Tested with / known bugs ====
+
+Here is a list of servers tested with this resource with the URLs used.
+Feel free to contact the author(s) if you successfully tested a configuration
+not listed here.
+
+* Egroupware (1.6.002)
+  https://host/groupdav.php
+  - GroupDAV working.
+  - CalDAV working.
+  - CardDAV working.
+
+* SOGo (version 1.0.4, 1.1.0 and version at http://sogo-demo.inverse.ca/)
+  https://host/SOGo/dav/<USER>/Calendar and https://host/SOGo/dav/<USER>/Contacts
+  - GroupDAV calendar working, but the timezone data in the ICalendar
+    generated by KCal seems misinterpreted by SOGo : every event is
+    shifted by the timezone offset (at least test with TZ Europe/Paris,
+    feel free to send your results to the author(s). This seems resolved
+    with the demo version made available by Inverse.ca.
+  - CalDAV working, with the same bug.
+  - CardDAV working.
+
+* Davical (version 0.9.7.6)
+  https://host/caldav.php/principals/users/<USER>
+  - CalDAV working.
+
+* Zimbra Open-Source edition (version 5.0.18),
+  https://host/principals/users/<USER>
+  - Caldav mostly working : retrieval of shared calendars that contain a lot
+    of events fails with a 500 server error.
+
+* Google calendar
+  https://www.google.com/calendar/dav/<CALENDAR_ID>/events
+  - CalDAV working.
diff --git a/resources/dav/TODO b/resources/dav/TODO
new file mode 100644 (file)
index 0000000..51e2051
--- /dev/null
@@ -0,0 +1,2 @@
+- see how to get 401 responses and ask the user the new password (and update
+  KWallet).
diff --git a/resources/dav/common/davcollection.cpp b/resources/dav/common/davcollection.cpp
new file mode 100644 (file)
index 0000000..83f9ff4
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+    Copyright (c) 2009 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davcollection.h"
+
+DavCollection::DavCollection()
+{
+}
+
+DavCollection::DavCollection(DavUtils::Protocol protocol, const QString &url, const QString &displayName, ContentTypes contentTypes)
+    : mProtocol(protocol), mUrl(url), mDisplayName(displayName), mContentTypes(contentTypes), mPrivileges(DavUtils::All)
+{
+}
+
+void DavCollection::setProtocol(DavUtils::Protocol protocol)
+{
+    mProtocol = protocol;
+}
+
+DavUtils::Protocol DavCollection::protocol() const
+{
+    return mProtocol;
+}
+
+void DavCollection::setCTag(const QString &ctag)
+{
+    mCTag = ctag;
+}
+
+QString DavCollection::CTag() const
+{
+    return mCTag;
+}
+
+void DavCollection::setUrl(const QString &url)
+{
+    mUrl = url;
+}
+
+QString DavCollection::url() const
+{
+    return mUrl;
+}
+
+void DavCollection::setDisplayName(const QString &displayName)
+{
+    mDisplayName = displayName;
+}
+
+QString DavCollection::displayName() const
+{
+    return mDisplayName;
+}
+
+void DavCollection::setColor(const QColor &color)
+{
+    mColor = color;
+}
+
+QColor DavCollection::color() const
+{
+    return mColor;
+}
+
+void DavCollection::setContentTypes(ContentTypes contentTypes)
+{
+    mContentTypes = contentTypes;
+}
+
+DavCollection::ContentTypes DavCollection::contentTypes() const
+{
+    return mContentTypes;
+}
+
+void DavCollection::setPrivileges(DavUtils::Privileges privs)
+{
+    mPrivileges = privs;
+}
+
+DavUtils::Privileges DavCollection::privileges() const
+{
+    return mPrivileges;
+}
+
diff --git a/resources/dav/common/davcollection.h b/resources/dav/common/davcollection.h
new file mode 100644 (file)
index 0000000..8ea52fb
--- /dev/null
@@ -0,0 +1,154 @@
+/*
+    Copyright (c) 2009 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVCOLLECTION_H
+#define DAVCOLLECTION_H
+
+#include "davutils.h"
+
+#include <QColor>
+#include <QtCore/QVector>
+#include <QtCore/QString>
+
+/**
+ * @short A helper class to store information about DAV collection.
+ *
+ * This class is used as container to transfer information about DAV
+ * collections between the Akonadi resource and the DAV jobs.
+ */
+class DavCollection
+{
+public:
+    /**
+     * Defines a list of DAV collection objects.
+     */
+    typedef QVector<DavCollection> List;
+
+    /**
+     * Describes the possible content type of the DAV collection.
+     */
+    enum ContentType {
+        Events = 1,    ///< The collection can contain event DAV resources.
+        Todos = 2,     ///< The collection can contain todo DAV resources.
+        Contacts = 4,  ///< The collection can contain contact DAV resources.
+        FreeBusy = 8,  ///< The collection can contain free/busy information.
+        Journal = 16,  ///< The collection can contain journal DAV resources.
+        Calendar = 32  ///< The collection can contain anything calendar-related.
+    };
+    Q_DECLARE_FLAGS(ContentTypes, ContentType)
+
+    /**
+     * Creates an empty DAV collection.
+     */
+    DavCollection();
+
+    /**
+     * Creates a new DAV collection.
+     *
+     * @param protocol The DAV protocol dialect the collection comes from.
+     * @param url The url that identifies the collection.
+     * @param displayName The display name of the collection.
+     * @param contentTypes The possible content types of the collection.
+     */
+    DavCollection(DavUtils::Protocol protocol, const QString &url, const QString &displayName, ContentTypes contentTypes);
+
+    /**
+     * Sets the DAV @p protocol dialect the collection comes from.
+     */
+    void setProtocol(DavUtils::Protocol protocol);
+
+    /**
+     * Returns the DAV protocol dialect the collection comes from.
+     */
+    DavUtils::Protocol protocol() const;
+
+    /**
+     * Sets this collection CTag.
+     */
+    void setCTag(const QString &ctag);
+
+    /**
+     * Returns this collection CTag. The returned value will be empty
+     * if no CTag was found.
+     */
+    QString CTag() const;
+
+    /**
+     * Sets the @p url that identifies the collection.
+     */
+    void setUrl(const QString &url);
+
+    /**
+     * Returns the url that identifies the collection.
+     */
+    QString url() const;
+
+    /**
+     * Sets the display @p name of the collection.
+     */
+    void setDisplayName(const QString &name);
+
+    /**
+     * Returns the display name of the collection.
+     */
+    QString displayName() const;
+
+    /**
+     * Sets the color for this collection
+     */
+    void setColor(const QColor &color);
+
+    /**
+     * Return the color of the collection, or an empty string if
+     * none was provided by the backend.
+     */
+    QColor color() const;
+
+    /**
+     * Sets the possible content @p types of the collection.
+     */
+    void setContentTypes(ContentTypes types);
+
+    /**
+     * Returns the possible content types of the collection.
+     */
+    ContentTypes contentTypes() const;
+
+    /**
+     * Sets the privileges on this collection.
+     */
+    void setPrivileges(DavUtils::Privileges privs);
+
+    /**
+     * Returns the privileges on this collection.
+     */
+    DavUtils::Privileges privileges() const;
+
+private:
+    DavUtils::Protocol mProtocol;
+    QString mCTag;
+    QString mUrl;
+    QString mDisplayName;
+    QColor mColor;
+    ContentTypes mContentTypes;
+    DavUtils::Privileges mPrivileges;
+};
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(DavCollection::ContentTypes)
+Q_DECLARE_TYPEINFO(DavCollection, Q_MOVABLE_TYPE);
+#endif
diff --git a/resources/dav/common/davcollectiondeletejob.cpp b/resources/dav/common/davcollectiondeletejob.cpp
new file mode 100644 (file)
index 0000000..3f05a9b
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+    Copyright (c) 2010 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davcollectiondeletejob.h"
+
+#include <kio/deletejob.h>
+#include <kio/job.h>
+#include <KLocalizedString>
+
+DavCollectionDeleteJob::DavCollectionDeleteJob(const DavUtils::DavUrl &url, QObject *parent)
+    : KJob(parent), mUrl(url)
+{
+}
+
+void DavCollectionDeleteJob::start()
+{
+    KIO::DeleteJob *job = KIO::del(mUrl.url(), KIO::HideProgressInfo | KIO::DefaultFlags);
+    job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
+    job->addMetaData(QStringLiteral("cookies"), QStringLiteral("none"));
+    job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
+
+    connect(job, &KIO::DeleteJob::result, this, &DavCollectionDeleteJob::davJobFinished);
+}
+
+void DavCollectionDeleteJob::davJobFinished(KJob *job)
+{
+    KIO::DeleteJob *deleteJob = qobject_cast<KIO::DeleteJob *>(job);
+
+    if (deleteJob->error() && deleteJob->error() != KIO::ERR_NO_CONTENT) {
+        const int responseCode = deleteJob->queryMetaData(QStringLiteral("responsecode")).isEmpty() ?
+                                 0 :
+                                 deleteJob->queryMetaData(QStringLiteral("responsecode")).toInt();
+
+        QString err;
+        if (deleteJob->error() != KIO::ERR_SLAVE_DEFINED) {
+            err = KIO::buildErrorString(deleteJob->error(), deleteJob->errorText());
+        } else {
+            err = deleteJob->errorText();
+        }
+
+        setError(UserDefinedError + responseCode);
+        setErrorText(i18n("There was a problem with the request. The collection has not been deleted from the server.\n"
+                          "%1 (%2).", err, responseCode));
+    }
+
+    emitResult();
+}
+
diff --git a/resources/dav/common/davcollectiondeletejob.h b/resources/dav/common/davcollectiondeletejob.h
new file mode 100644 (file)
index 0000000..9c30654
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+    Copyright (c) 2010 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVCOLLECTIONDELETEJOB_H
+#define DAVCOLLECTIONDELETEJOB_H
+
+#include "davutils.h"
+
+#include <kjob.h>
+
+/**
+ * @short A job that deletes a DAV collection.
+ *
+ * This job is used to delete a DAV collection at a certain URL.
+ */
+class DavCollectionDeleteJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new DAV collection delete job.
+     *
+     * @param url The dav url of the collection to delete
+     * @param parent The parent object.
+     */
+    explicit DavCollectionDeleteJob(const DavUtils::DavUrl &url, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Starts the job.
+     */
+    void start() Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void davJobFinished(KJob *);
+
+private:
+    DavUtils::DavUrl mUrl;
+};
+
+#endif
+
diff --git a/resources/dav/common/davcollectionmodifyjob.cpp b/resources/dav/common/davcollectionmodifyjob.cpp
new file mode 100644 (file)
index 0000000..db70768
--- /dev/null
@@ -0,0 +1,163 @@
+/*
+    Copyright (c) 2010 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davcollectionmodifyjob.h"
+#include "davmanager.h"
+#include "davutils.h"
+
+#include <kio/davjob.h>
+#include <kio/job.h>
+#include <KLocalizedString>
+
+DavCollectionModifyJob::DavCollectionModifyJob(const DavUtils::DavUrl &url, QObject *parent)
+    : KJob(parent), mUrl(url)
+{
+}
+
+void DavCollectionModifyJob::setProperty(const QString &prop, const QString &value, const QString &ns)
+{
+    QDomElement propElement;
+
+    if (ns.isEmpty()) {
+        propElement = mQuery.createElement(prop);
+    } else {
+        propElement = mQuery.createElementNS(prop, ns);
+    }
+
+    const QDomText textElement = mQuery.createTextNode(value);
+    propElement.appendChild(textElement);
+
+    mSetProperties << propElement;
+}
+
+void DavCollectionModifyJob::removeProperty(const QString &prop, const QString &ns)
+{
+    QDomElement propElement;
+
+    if (ns.isEmpty()) {
+        propElement = mQuery.createElement(prop);
+    } else {
+        propElement = mQuery.createElementNS(prop, ns);
+    }
+
+    mRemoveProperties << propElement;
+}
+
+void DavCollectionModifyJob::start()
+{
+    if (mSetProperties.isEmpty() && mRemoveProperties.isEmpty()) {
+        setError(UserDefinedError);   // no special meaning, for now at least
+        setErrorText(i18n("No properties to change or remove"));
+        emitResult();
+        return;
+    }
+
+    QDomDocument mQuery;
+    QDomElement propertyUpdateElement = mQuery.createElementNS(QStringLiteral("DAV:"), QStringLiteral("propertyupdate"));
+    mQuery.appendChild(propertyUpdateElement);
+
+    if (!mSetProperties.isEmpty()) {
+        QDomElement setElement = mQuery.createElementNS(QStringLiteral("DAV:"), QStringLiteral("set"));
+        propertyUpdateElement.appendChild(setElement);
+
+        QDomElement propElement = mQuery.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
+        setElement.appendChild(propElement);
+
+        foreach (const QDomElement &element, mSetProperties) {
+            propElement.appendChild(element);
+        }
+    }
+
+    if (!mRemoveProperties.isEmpty()) {
+        QDomElement removeElement = mQuery.createElementNS(QStringLiteral("DAV:"), QStringLiteral("remove"));
+        propertyUpdateElement.appendChild(removeElement);
+
+        QDomElement propElement = mQuery.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
+        removeElement.appendChild(propElement);
+
+        foreach (const QDomElement &element, mSetProperties) {
+            propElement.appendChild(element);
+        }
+    }
+
+    KIO::DavJob *job = DavManager::self()->createPropPatchJob(mUrl.url(), mQuery);
+    job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
+    connect(job, &KIO::DavJob::result, this, &DavCollectionModifyJob::davJobFinished);
+}
+
+void DavCollectionModifyJob::davJobFinished(KJob *job)
+{
+    KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
+    const int responseCode = davJob->queryMetaData(QStringLiteral("responsecode")).isEmpty() ?
+                             0 :
+                             davJob->queryMetaData(QStringLiteral("responsecode")).toInt();
+
+    // KIO::DavJob does not set error() even if the HTTP status code is a 4xx or a 5xx
+    if (davJob->error() || (responseCode >= 400 && responseCode < 600)) {
+        QString err;
+        if (davJob->error() && davJob->error() != KIO::ERR_SLAVE_DEFINED) {
+            err = KIO::buildErrorString(davJob->error(), davJob->errorText());
+        } else {
+            err = davJob->errorText();
+        }
+
+        setError(UserDefinedError + responseCode);
+        setErrorText(i18n("There was a problem with the request. The item has not been modified on the server.\n"
+                          "%1 (%2).", err, responseCode));
+
+        emitResult();
+        return;
+    }
+
+    const QDomDocument response = davJob->response();
+    QDomElement responseElement = DavUtils::firstChildElementNS(response.documentElement(), QStringLiteral("DAV:"), QStringLiteral("response"));
+
+    bool hasError = false;
+    QString errorText;
+
+    // parse all propstats answers to get the eventual errors
+    const QDomNodeList propstats = responseElement.elementsByTagNameNS(QStringLiteral("DAV:"), QStringLiteral("propstat"));
+    for (int i = 0; i < propstats.length(); ++i) {
+        const QDomElement propstatElement = propstats.item(i).toElement();
+        const QDomElement statusElement = DavUtils::firstChildElementNS(propstatElement, QStringLiteral("DAV:"), QStringLiteral("status"));
+
+        const QString statusText = statusElement.text();
+        if (statusText.contains(QStringLiteral("200"))) {
+            // Nothing special to do here, this indicates the success of the whole request
+            break;
+        } else {
+            // Generic error
+            hasError = true;
+            errorText = i18n("There was an error when modifying the properties");
+        }
+    }
+
+    if (hasError) {
+        // Trying to get more information about the error
+        const QDomElement responseDescriptionElement = DavUtils::firstChildElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("responsedescription"));
+        if (!responseDescriptionElement.isNull()) {
+            errorText.append(i18n("\nThe server returned more information:\n"));
+            errorText.append(responseDescriptionElement.text());
+        }
+
+        setError(UserDefinedError);
+        setErrorText(errorText);
+    }
+
+    emitResult();
+}
diff --git a/resources/dav/common/davcollectionmodifyjob.h b/resources/dav/common/davcollectionmodifyjob.h
new file mode 100644 (file)
index 0000000..d5cce68
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+    Copyright (c) 2010 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVCOLLECTIONMODIFYJOB_H
+#define DAVCOLLECTIONMODIFYJOB_H
+
+#include "davutils.h"
+
+#include <QtCore/QList>
+
+#include <kjob.h>
+
+/**
+ * @short A job that modifies a DAV collection.
+ *
+ * This job is used to modify a property of a DAV collection
+ * on the DAV server.
+ */
+class DavCollectionModifyJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new dav collection modify job.
+     *
+     * @param url The DAV url that identifies the collection.
+     * @param parent The parent object.
+     */
+    explicit DavCollectionModifyJob(const DavUtils::DavUrl &url, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Sets the property that shall be modified by the job.
+     *
+     * @param property The name of the property.
+     * @param value The value of the property.
+     * @param ns The XML namespace that shall be used for the property name.
+     */
+    void setProperty(const QString &property, const QString &value, const QString &ns = QString());
+
+    /**
+     * Sets the property that shall be removed by the job.
+     *
+     * @param property The name of the property.
+     * @param ns The XML namespace that shall be used for the property name.
+     */
+    void removeProperty(const QString &property, const QString &ns);
+
+    /**
+     * Starts the job.
+     */
+    void start() Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void davJobFinished(KJob *job);
+
+private:
+    DavUtils::DavUrl mUrl;
+    QDomDocument mQuery;
+
+    QVector<QDomElement> mSetProperties;
+    QVector<QDomElement> mRemoveProperties;
+};
+
+#endif
diff --git a/resources/dav/common/davcollectionsfetchjob.cpp b/resources/dav/common/davcollectionsfetchjob.cpp
new file mode 100644 (file)
index 0000000..51bd3f0
--- /dev/null
@@ -0,0 +1,347 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davcollectionsfetchjob.h"
+
+#include "davmanager.h"
+#include "davprincipalhomesetsfetchjob.h"
+#include "davprotocolbase.h"
+#include "davutils.h"
+
+#include <qdebug.h>
+#include <kio/davjob.h>
+#include <kio/job.h>
+#include <KLocalizedString>
+
+#include <QtCore/QBuffer>
+#include <QtXmlPatterns/QXmlQuery>
+
+DavCollectionsFetchJob::DavCollectionsFetchJob(const DavUtils::DavUrl &url, QObject *parent)
+    : KJob(parent), mUrl(url), mSubJobCount(0)
+{
+}
+
+void DavCollectionsFetchJob::start()
+{
+    if (DavManager::self()->davProtocol(mUrl.protocol())->supportsPrincipals()) {
+        DavPrincipalHomeSetsFetchJob *job = new DavPrincipalHomeSetsFetchJob(mUrl);
+        connect(job, &DavPrincipalHomeSetsFetchJob::result, this, &DavCollectionsFetchJob::principalFetchFinished);
+        job->start();
+    } else {
+        doCollectionsFetch(mUrl.url());
+    }
+}
+
+DavCollection::List DavCollectionsFetchJob::collections() const
+{
+    return mCollections;
+}
+
+DavUtils::DavUrl DavCollectionsFetchJob::davUrl() const
+{
+    return mUrl;
+}
+
+void DavCollectionsFetchJob::doCollectionsFetch(const QUrl &url)
+{
+    ++mSubJobCount;
+
+    const QDomDocument collectionQuery = DavManager::self()->davProtocol(mUrl.protocol())->collectionsQuery()->buildQuery();
+
+    KIO::DavJob *job = DavManager::self()->createPropFindJob(url, collectionQuery);
+    connect(job, &KIO::DavJob::result, this, &DavCollectionsFetchJob::collectionsFetchFinished);
+    job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
+}
+
+void DavCollectionsFetchJob::principalFetchFinished(KJob *job)
+{
+    const DavPrincipalHomeSetsFetchJob *davJob = qobject_cast<DavPrincipalHomeSetsFetchJob *>(job);
+
+    if (davJob->error()) {
+        if (davJob->latestResponseCode()) {
+            // If we have a HTTP response code then this may mean that
+            // the URL was not a principal URL. Retry as if it were a calendar URL.
+            qDebug() << job->errorText();
+            doCollectionsFetch(mUrl.url());
+        } else {
+            // Just give up here.
+            setError(davJob->error());
+            setErrorText(davJob->errorText());
+            emitResult();
+        }
+
+        return;
+    }
+
+    const QStringList homeSets = davJob->homeSets();
+    qDebug() << "Found " << homeSets.size() << " homesets";
+    qDebug() << homeSets;
+
+    if (homeSets.isEmpty()) {
+        // Same as above, retry as if it were a calendar URL.
+        doCollectionsFetch(mUrl.url());
+        return;
+    }
+
+    foreach (const QString &homeSet, homeSets) {
+        QUrl url = mUrl.url();
+
+        if (homeSet.startsWith(QLatin1Char('/'))) {
+            // homeSet is only a path, use request url to complete
+            url.setPath(homeSet, QUrl::TolerantMode);
+        } else {
+            // homeSet is a complete url
+            QUrl tmpUrl(homeSet);
+            tmpUrl.setUserName(url.userName());
+            tmpUrl.setPassword(url.password());
+            url = tmpUrl;
+        }
+
+        doCollectionsFetch(url);
+    }
+}
+
+void DavCollectionsFetchJob::collectionsFetchFinished(KJob *job)
+{
+    KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
+    const int responseCode = davJob->queryMetaData(QStringLiteral("responsecode")).isEmpty() ?
+                             0 :
+                             davJob->queryMetaData(QStringLiteral("responsecode")).toInt();
+
+    // KIO::DavJob does not set error() even if the HTTP status code is a 4xx or a 5xx
+    if (davJob->error() || (responseCode >= 400 && responseCode < 600)) {
+        if (davJob->url() != mUrl.url()) {
+            // Retry as if the initial URL was a calendar URL.
+            // We can end up here when retrieving a homeset on
+            // which a PROPFIND resulted in an error
+            doCollectionsFetch(mUrl.url());
+            --mSubJobCount;
+            return;
+        }
+
+        QString err;
+        if (davJob->error() && davJob->error() != KIO::ERR_SLAVE_DEFINED) {
+            err = KIO::buildErrorString(davJob->error(), davJob->errorText());
+        } else {
+            err = davJob->errorText();
+        }
+
+        setError(UserDefinedError + responseCode);
+        setErrorText(i18n("There was a problem with the request.\n"
+                          "%1 (%2).", err, responseCode));
+    } else {
+        // For use in the collectionDiscovered() signal
+        QUrl _jobUrl = mUrl.url();
+        _jobUrl.setUserInfo(QString());
+        const QString jobUrl = _jobUrl.toDisplayString();
+
+        // Validate that we got a valid PROPFIND response
+        QDomElement rootElement = davJob->response().documentElement();
+        if (rootElement.tagName().compare(QStringLiteral("multistatus"), Qt::CaseInsensitive) != 0) {
+            setError(UserDefinedError);
+            setErrorText(i18n("Invalid responses from backend"));
+            subjobFinished();
+            return;
+        }
+
+        QByteArray resp(davJob->response().toByteArray());
+        QBuffer buffer(&resp);
+        buffer.open(QIODevice::ReadOnly);
+
+        QXmlQuery xquery;
+        if (!xquery.setFocus(&buffer)) {
+            setError(UserDefinedError);
+            setErrorText(i18n("Error setting focus for XQuery"));
+            subjobFinished();
+            return;
+        }
+
+        xquery.setQuery(DavManager::self()->davProtocol(mUrl.protocol())->collectionsXQuery());
+        if (!xquery.isValid()) {
+            setError(UserDefinedError);
+            setErrorText(i18n("Invalid XQuery submitted by DAV implementation"));
+            subjobFinished();
+            return;
+        }
+
+        QString responsesStr;
+        xquery.evaluateTo(&responsesStr);
+        responsesStr.prepend(QStringLiteral("<responses>"));
+        responsesStr.append(QStringLiteral("</responses>"));
+
+        QDomDocument document;
+        if (!document.setContent(responsesStr, true)) {
+            setError(UserDefinedError);
+            setErrorText(i18n("Invalid responses from backend"));
+            subjobFinished();
+            return;
+        }
+
+        if (!error()) {
+            /*
+             * Extract information from a document like the following:
+             *
+             * <responses>
+             *   <response xmlns="DAV:">
+             *     <href xmlns="DAV:">/caldav.php/test1.user/home/</href>
+             *     <propstat xmlns="DAV:">
+             *       <prop xmlns="DAV:">
+             *         <C:supported-calendar-component-set xmlns:C="urn:ietf:params:xml:ns:caldav">
+             *           <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VEVENT"/>
+             *           <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VTODO"/>
+             *           <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VJOURNAL"/>
+             *           <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VTIMEZONE"/>
+             *           <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VFREEBUSY"/>
+             *         </C:supported-calendar-component-set>
+             *         <resourcetype xmlns="DAV:">
+             *           <collection xmlns="DAV:"/>
+             *           <C:calendar xmlns:C="urn:ietf:params:xml:ns:caldav"/>
+             *           <C:schedule-calendar xmlns:C="urn:ietf:params:xml:ns:caldav"/>
+             *         </resourcetype>
+             *         <displayname xmlns="DAV:">Test1 User</displayname>
+             *         <current-user-privilege-set xmlns="DAV:">
+             *           <privilege xmlns="DAV:">
+             *             <read xmlns="DAV:"/>
+             *           </privilege>
+             *         </current-user-privilege-set>
+             *         <getctag xmlns="http://calendarserver.org/ns/">12345</getctag>
+             *       </prop>
+             *       <status xmlns="DAV:">HTTP/1.1 200 OK</status>
+             *     </propstat>
+             *   </response>
+             * </responses>
+             */
+
+            const QDomElement responsesElement = document.documentElement();
+
+            QDomElement responseElement = DavUtils::firstChildElementNS(responsesElement, QStringLiteral("DAV:"), QStringLiteral("response"));
+            while (!responseElement.isNull()) {
+
+                QDomElement propstatElement;
+
+                // check for the valid propstat, without giving up on first error
+                {
+                    const QDomNodeList propstats = responseElement.elementsByTagNameNS(QStringLiteral("DAV:"), QStringLiteral("propstat"));
+                    for (int i = 0; i < propstats.length(); ++i) {
+                        const QDomElement propstatCandidate = propstats.item(i).toElement();
+                        const QDomElement statusElement = DavUtils::firstChildElementNS(propstatCandidate, QStringLiteral("DAV:"), QStringLiteral("status"));
+                        if (statusElement.text().contains(QStringLiteral("200"))) {
+                            propstatElement = propstatCandidate;
+                        }
+                    }
+                }
+
+                if (propstatElement.isNull()) {
+                    responseElement = DavUtils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
+                    continue;
+                }
+
+                // extract url
+                const QDomElement hrefElement = DavUtils::firstChildElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("href"));
+                if (hrefElement.isNull()) {
+                    responseElement = DavUtils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
+                    continue;
+                }
+
+                QString href = hrefElement.text();
+                if (!href.endsWith(QLatin1Char('/'))) {
+                    href.append(QLatin1Char('/'));
+                }
+
+                QUrl url = davJob->url();
+                url.setUserInfo(QString());
+                if (href.startsWith(QLatin1Char('/'))) {
+                    // href is only a path, use request url to complete
+                    url.setPath(href, QUrl::TolerantMode);
+                } else {
+                    // href is a complete url
+                    url = QUrl::fromUserInput(href);
+                }
+
+                // don't add this resource if it has already been detected
+                bool alreadySeen = false;
+                foreach (const DavCollection &seen, mCollections) {
+                    if (seen.url() == url.toDisplayString()) {
+                        alreadySeen = true;
+                    }
+                }
+                if (alreadySeen) {
+                    responseElement = DavUtils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
+                    continue;
+                }
+
+                // extract display name
+                const QDomElement propElement = DavUtils::firstChildElementNS(propstatElement, QStringLiteral("DAV:"), QStringLiteral("prop"));
+                const QDomElement displaynameElement = DavUtils::firstChildElementNS(propElement, QStringLiteral("DAV:"), QStringLiteral("displayname"));
+                const QString displayName = displaynameElement.text();
+
+                // Extract CTag
+                const QDomElement CTagElement = DavUtils::firstChildElementNS(propElement, QStringLiteral("http://calendarserver.org/ns/"), QStringLiteral("getctag"));
+                QString CTag;
+                if (!CTagElement.isNull()) {
+                    CTag = CTagElement.text();
+                }
+
+                // extract calendar color if provided
+                const QDomElement colorElement = DavUtils::firstChildElementNS(propElement, QStringLiteral("http://apple.com/ns/ical/"), QStringLiteral("calendar-color"));
+                QColor color;
+                if (!colorElement.isNull()) {
+                    QString colorValue = colorElement.text();
+                    if (QColor::isValidColor(colorValue)) {
+                        color.setNamedColor(colorValue);
+                    }
+                }
+
+                // extract allowed content types
+                const DavCollection::ContentTypes contentTypes = DavManager::self()->davProtocol(mUrl.protocol())->collectionContentTypes(propstatElement);
+
+                DavCollection collection(mUrl.protocol(), url.toDisplayString(), displayName, contentTypes);
+                collection.setCTag(CTag);
+                if (color.isValid()) {
+                    collection.setColor(color);
+                }
+
+                // extract privileges
+                const QDomElement currentPrivsElement = DavUtils::firstChildElementNS(propElement, QStringLiteral("DAV:"), QStringLiteral("current-user-privilege-set"));
+                if (currentPrivsElement.isNull()) {
+                    // Assume that we have all privileges
+                    collection.setPrivileges(DavUtils::All);
+                } else {
+                    DavUtils::Privileges privileges = DavUtils::extractPrivileges(currentPrivsElement);
+                    collection.setPrivileges(privileges);
+                }
+
+                qDebug() << url.toDisplayString() << "PRIVS: " << collection.privileges();
+                mCollections << collection;
+                Q_EMIT collectionDiscovered(mUrl.protocol(), url.toDisplayString(), jobUrl);
+
+                responseElement = DavUtils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
+            }
+        }
+    }
+
+    subjobFinished();
+}
+
+void DavCollectionsFetchJob::subjobFinished()
+{
+    if (--mSubJobCount == 0) {
+        emitResult();
+    }
+}
+
diff --git a/resources/dav/common/davcollectionsfetchjob.h b/resources/dav/common/davcollectionsfetchjob.h
new file mode 100644 (file)
index 0000000..0905090
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVCOLLECTIONSFETCHJOB_H
+#define DAVCOLLECTIONSFETCHJOB_H
+
+#include "davcollection.h"
+#include "davutils.h"
+
+#include <kjob.h>
+
+/**
+ * @short A job that fetches all DAV collection.
+ *
+ * This job is used to fetch all DAV collection that are available
+ * under a certain DAV url.
+ */
+class DavCollectionsFetchJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new dav collections fetch job.
+     *
+     * @param url The DAV url of the DAV collection whose sub collections shall be fetched.
+     * @param parent The parent object.
+     */
+    explicit DavCollectionsFetchJob(const DavUtils::DavUrl &url, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Starts the job.
+     */
+    void start() Q_DECL_OVERRIDE;
+
+    /**
+     * Returns the list of fetched DAV collections.
+     */
+    DavCollection::List collections() const;
+
+    /**
+     * Return the DavUrl used by this job
+     */
+    DavUtils::DavUrl davUrl() const;
+
+Q_SIGNALS:
+    /**
+     * This signal is emitted every time a new collection has been discovered.
+     *
+     * @param collectionUrl The URL of the discovered collection
+     * @param configuredUrl The URL given to the job
+     */
+    void collectionDiscovered(int protocol, const QString &collectionUrl, const QString &configuredUrl);
+
+private Q_SLOTS:
+    void principalFetchFinished(KJob *);
+    void collectionsFetchFinished(KJob *);
+
+private:
+    void doCollectionsFetch(const QUrl &url);
+    void subjobFinished();
+
+    DavUtils::DavUrl mUrl;
+    DavCollection::List mCollections;
+    uint mSubJobCount;
+};
+
+#endif
diff --git a/resources/dav/common/davcollectionsmultifetchjob.cpp b/resources/dav/common/davcollectionsmultifetchjob.cpp
new file mode 100644 (file)
index 0000000..6450b6f
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davcollectionsmultifetchjob.h"
+
+#include "davcollectionsfetchjob.h"
+
+DavCollectionsMultiFetchJob::DavCollectionsMultiFetchJob(const DavUtils::DavUrl::List &urls, QObject *parent)
+    : KJob(parent), mUrls(urls), mSubJobCount(urls.size())
+{
+}
+
+void DavCollectionsMultiFetchJob::start()
+{
+    if (mUrls.isEmpty()) {
+        emitResult();
+    }
+
+    foreach (const DavUtils::DavUrl &url, mUrls) {
+        DavCollectionsFetchJob *job = new DavCollectionsFetchJob(url, this);
+        connect(job, &DavCollectionsFetchJob::result, this, &DavCollectionsMultiFetchJob::davJobFinished);
+        connect(job, &DavCollectionsFetchJob::collectionDiscovered, this, &DavCollectionsMultiFetchJob::collectionDiscovered);
+        job->start();
+    }
+}
+
+DavCollection::List DavCollectionsMultiFetchJob::collections() const
+{
+    return mCollections;
+}
+
+void DavCollectionsMultiFetchJob::davJobFinished(KJob *job)
+{
+    DavCollectionsFetchJob *fetchJob = qobject_cast<DavCollectionsFetchJob *>(job);
+
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+    } else {
+        mCollections << fetchJob->collections();
+    }
+
+    if (--mSubJobCount == 0) {
+        emitResult();
+    }
+}
+
diff --git a/resources/dav/common/davcollectionsmultifetchjob.h b/resources/dav/common/davcollectionsmultifetchjob.h
new file mode 100644 (file)
index 0000000..d707b76
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVCOLLECTIONSMULTIFETCHJOB_H
+#define DAVCOLLECTIONSMULTIFETCHJOB_H
+
+#include "davcollection.h"
+#include "davutils.h"
+
+#include <kjob.h>
+
+/**
+ * @short A job that fetches all DAV collection.
+ *
+ * This job is used to fetch all DAV collection that are available
+ * under a certain list of DAV urls.
+ *
+ * @note This class just combines multiple calls of DavCollectionsFetchJob
+ *       into one job.
+ */
+class DavCollectionsMultiFetchJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new dav collections multi fetch job.
+     *
+     * @param urls The list of DAV urls whose sub collections shall be fetched.
+     * @param parent The parent object.
+     */
+    explicit DavCollectionsMultiFetchJob(const DavUtils::DavUrl::List &urls, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Starts the job.
+     */
+    void start() Q_DECL_OVERRIDE;
+
+    /**
+     * Returns the list of fetched DAV collections.
+     */
+    DavCollection::List collections() const;
+
+Q_SIGNALS:
+    /**
+     * This signal is emitted every time a new collection has been discovered.
+     *
+     * @param collectionUrl The URL of the discovered collection
+     * @param configuredUrl The URL given to the job
+     */
+    void collectionDiscovered(int protocol, const QString &collectionUrl, const QString &configuredUrl);
+
+private Q_SLOTS:
+    void davJobFinished(KJob *);
+
+private:
+    DavUtils::DavUrl::List mUrls;
+    DavCollection::List mCollections;
+    uint mSubJobCount;
+};
+
+#endif
diff --git a/resources/dav/common/davitem.cpp b/resources/dav/common/davitem.cpp
new file mode 100644 (file)
index 0000000..e5c5154
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+    Copyright (c) 2009 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davitem.h"
+
+DavItem::DavItem()
+{
+}
+
+DavItem::DavItem(const QString &url, const QString &contentType, const QByteArray &data, const QString &etag)
+    : mUrl(url), mContentType(contentType), mData(data), mEtag(etag)
+{
+}
+
+void DavItem::setUrl(const QString &url)
+{
+    mUrl = url;
+}
+
+QString DavItem::url() const
+{
+    return mUrl;
+}
+
+void DavItem::setContentType(const QString &contentType)
+{
+    mContentType = contentType;
+}
+
+QString DavItem::contentType() const
+{
+    return mContentType;
+}
+
+void DavItem::setData(const QByteArray &data)
+{
+    mData = data;
+}
+
+QByteArray DavItem::data() const
+{
+    return mData;
+}
+
+void DavItem::setEtag(const QString &etag)
+{
+    mEtag = etag;
+}
+
+QString DavItem::etag() const
+{
+    return mEtag;
+}
+
+QDataStream &operator<<(QDataStream &stream, const DavItem &item)
+{
+    stream << item.url();
+    stream << item.contentType();
+    stream << item.data();
+    stream << item.etag();
+
+    return stream;
+}
+
+QDataStream &operator>>(QDataStream &stream, DavItem &item)
+{
+    QString url, contentType, etag;
+    QByteArray data;
+
+    stream >> url;
+    stream >> contentType;
+    stream >> data;
+    stream >> etag;
+
+    item = DavItem(url, contentType, data, etag);
+
+    return stream;
+}
diff --git a/resources/dav/common/davitem.h b/resources/dav/common/davitem.h
new file mode 100644 (file)
index 0000000..12cb96e
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+    Copyright (c) 2009 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVITEM_H
+#define DAVITEM_H
+
+#include <QtCore/QByteArray>
+#include <QtCore/QDataStream>
+#include <QtCore/QList>
+#include <QtCore/QString>
+
+/**
+ * @short A helper class to store information about DAV resources.
+ *
+ * This class is used as container to transfer information about DAV
+ * resources between the Akonadi resource and the DAV jobs.
+ *
+ * @note While the DAV RFC names them DAV resource we call them items
+ *       to comply to Akonadi terminology.
+ */
+class DavItem
+{
+public:
+    /**
+     * Defines a list of DAV item objects.
+     */
+    typedef QVector<DavItem> List;
+
+    /**
+     * Creates an empty DAV item.
+     */
+    DavItem();
+
+    /**
+     * Creates a new DAV item.
+     *
+     * @param url The url that identifies the item.
+     * @param contentType The content type of the item.
+     * @param data The actual raw content data of the item.
+     * @param etag The etag of the item.
+     */
+    DavItem(const QString &url, const QString &contentType, const QByteArray &data, const QString &etag);
+
+    /**
+     * Sets the @p url that identifies the item.
+     */
+    void setUrl(const QString &url);
+
+    /**
+     * Returns the url that identifies the item.
+     */
+    QString url() const;
+
+    /**
+     * Sets the content @p type of the item.
+     */
+    void setContentType(const QString &type);
+
+    /**
+     * Returns the content type of the item.
+     */
+    QString contentType() const;
+
+    /**
+     * Sets the raw content @p data of the item.
+     */
+    void setData(const QByteArray &data);
+
+    /**
+     * Returns the raw content data of the item.
+     */
+    QByteArray data() const;
+
+    /**
+     * Sets the @p etag of the item.
+     */
+    void setEtag(const QString &etag);
+
+    /**
+     * Returns the etag of the item.
+     */
+    QString etag() const;
+
+private:
+    QString mUrl;
+    QString mContentType;
+    QByteArray mData;
+    QString mEtag;
+};
+
+QDataStream &operator<<(QDataStream &out, const DavItem &item);
+QDataStream &operator>>(QDataStream &in, DavItem &item);
+Q_DECLARE_TYPEINFO(DavItem, Q_MOVABLE_TYPE);
+#endif
diff --git a/resources/dav/common/davitemcreatejob.cpp b/resources/dav/common/davitemcreatejob.cpp
new file mode 100644 (file)
index 0000000..2af041c
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davitemcreatejob.h"
+
+#include "davitemfetchjob.h"
+#include "davmanager.h"
+
+#include <kio/davjob.h>
+#include <kio/job.h>
+#include <KLocalizedString>
+
+#include <QtCore/QDebug>
+
+DavItemCreateJob::DavItemCreateJob(const DavUtils::DavUrl &url, const DavItem &item, QObject *parent)
+    : DavJobBase(parent), mUrl(url), mItem(item), mRedirectCount(0)
+{
+}
+
+void DavItemCreateJob::start()
+{
+    QString headers = QStringLiteral("Content-Type: ");
+    headers += mItem.contentType();
+    headers += QLatin1String("\r\n");
+    headers += QLatin1String("If-None-Match: *");
+
+    KIO::StoredTransferJob *job = KIO::storedPut(mItem.data(), mUrl.url(), -1, KIO::HideProgressInfo | KIO::DefaultFlags);
+    job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
+    job->addMetaData(QStringLiteral("customHTTPHeader"), headers);
+    job->addMetaData(QStringLiteral("cookies"), QStringLiteral("none"));
+    job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
+    job->setRedirectionHandlingEnabled(false);
+
+    connect(job, &KIO::StoredTransferJob::result, this, &DavItemCreateJob::davJobFinished);
+}
+
+DavItem DavItemCreateJob::item() const
+{
+    return mItem;
+}
+
+void DavItemCreateJob::davJobFinished(KJob *job)
+{
+    KIO::StoredTransferJob *storedJob = qobject_cast<KIO::StoredTransferJob *>(job);
+    const int responseCode = storedJob->queryMetaData(QStringLiteral("responsecode")).isEmpty() ?
+                             0 :
+                             storedJob->queryMetaData(QStringLiteral("responsecode")).toInt();
+
+    if (storedJob->error()) {
+        QString err;
+        if (storedJob->error() != KIO::ERR_SLAVE_DEFINED) {
+            err = KIO::buildErrorString(storedJob->error(), storedJob->errorText());
+        } else {
+            err = storedJob->errorText();
+        }
+
+        setLatestResponseCode(responseCode);
+        setError(UserDefinedError + responseCode);
+        setErrorText(i18n("There was a problem with the request. The item has not been created on the server.\n"
+                          "%1 (%2).", err, responseCode));
+
+        emitResult();
+        return;
+    }
+
+    // The 'Location:' HTTP header is used to indicate the new URL
+    const QStringList allHeaders = storedJob->queryMetaData(QStringLiteral("HTTP-Headers")).split(QLatin1Char('\n'));
+    QString location;
+    foreach (const QString &header, allHeaders) {
+        if (header.startsWith(QLatin1String("location:"), Qt::CaseInsensitive)) {
+            location = header.section(QLatin1Char(' '), 1);
+        }
+    }
+
+    QUrl url;
+    if (location.isEmpty()) {
+        url = storedJob->url();
+    } else if (location.startsWith(QLatin1Char('/'))) {
+        url = storedJob->url();
+        url.setPath(location, QUrl::TolerantMode);
+    } else {
+        url = QUrl::fromUserInput(location);
+    }
+
+    if (responseCode == 301 || responseCode == 302 || responseCode == 307 || responseCode == 308) {
+        if (mRedirectCount > 4) {
+            setLatestResponseCode(responseCode);
+            setError(UserDefinedError + responseCode);
+            emitResult();
+        } else {
+            QUrl itemUrl(url);
+            itemUrl.setUserName(mUrl.url().userName());
+            itemUrl.setPassword(mUrl.url().password());
+            mUrl.setUrl(itemUrl);
+
+            ++mRedirectCount;
+            start();
+        }
+
+        return;
+    }
+
+    url.setUserInfo(QString());
+    mItem.setUrl(url.toDisplayString());
+
+    DavItemFetchJob *fetchJob = new DavItemFetchJob(mUrl, mItem);
+    connect(fetchJob, &DavItemFetchJob::result, this, &DavItemCreateJob::itemRefreshed);
+    fetchJob->start();
+}
+
+void DavItemCreateJob::itemRefreshed(KJob *job)
+{
+    if (!job->error()) {
+        DavItemFetchJob *fetchJob = qobject_cast<DavItemFetchJob *>(job);
+        mItem.setEtag(fetchJob->item().etag());
+    }
+    emitResult();
+}
+
diff --git a/resources/dav/common/davitemcreatejob.h b/resources/dav/common/davitemcreatejob.h
new file mode 100644 (file)
index 0000000..42b69be
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVITEMCREATEJOB_H
+#define DAVITEMCREATEJOB_H
+
+#include "davitem.h"
+#include "davjobbase.h"
+#include "davutils.h"
+
+/**
+ * @short A job to create a DAV item on the DAV server.
+ */
+class DavItemCreateJob : public DavJobBase
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new dav item create job.
+     *
+     * @param url The target url where the item shall be created.
+     * @param item The item that shall be created.
+     * @param parent The parent object.
+     */
+    DavItemCreateJob(const DavUtils::DavUrl &url, const DavItem &item, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Starts the job.
+     */
+    void start() Q_DECL_OVERRIDE;
+
+    /**
+     * Returns the created DAV item including the correct identifier url
+     * and current etag information.
+     */
+    DavItem item() const;
+
+private Q_SLOTS:
+    void davJobFinished(KJob *);
+    void itemRefreshed(KJob *);
+
+private:
+    DavUtils::DavUrl mUrl;
+    DavItem mItem;
+    int mRedirectCount;
+};
+
+#endif
diff --git a/resources/dav/common/davitemdeletejob.cpp b/resources/dav/common/davitemdeletejob.cpp
new file mode 100644 (file)
index 0000000..4f425bd
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davitemdeletejob.h"
+
+#include "davitemfetchjob.h"
+#include "davmanager.h"
+
+#include <kio/deletejob.h>
+#include <kio/job.h>
+#include <KLocalizedString>
+
+DavItemDeleteJob::DavItemDeleteJob(const DavUtils::DavUrl &url, const DavItem &item, QObject *parent)
+    : DavJobBase(parent), mUrl(url), mItem(item), mFreshResponseCode(-1)
+{
+}
+
+void DavItemDeleteJob::start()
+{
+    KIO::DeleteJob *job = KIO::del(mUrl.url(), KIO::HideProgressInfo | KIO::DefaultFlags);
+    job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
+    job->addMetaData(QStringLiteral("customHTTPHeader"), QStringLiteral("If-Match: ") + mItem.etag());
+    job->addMetaData(QStringLiteral("cookies"), QStringLiteral("none"));
+    job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
+
+    connect(job, &KIO::DeleteJob::result, this, &DavItemDeleteJob::davJobFinished);
+}
+
+DavItem DavItemDeleteJob::freshItem() const
+{
+    return mFreshItem;
+}
+
+int DavItemDeleteJob::freshResponseCode() const
+{
+    return mFreshResponseCode;
+}
+
+void DavItemDeleteJob::davJobFinished(KJob *job)
+{
+    KIO::DeleteJob *deleteJob = qobject_cast<KIO::DeleteJob *>(job);
+
+    if (deleteJob->error() && deleteJob->error() != KIO::ERR_NO_CONTENT) {
+        const int responseCode = deleteJob->queryMetaData(QStringLiteral("responsecode")).isEmpty() ?
+                                 0 :
+                                 deleteJob->queryMetaData(QStringLiteral("responsecode")).toInt();
+
+        if (responseCode != 404 && responseCode != 410) {
+            QString err;
+            if (deleteJob->error() != KIO::ERR_SLAVE_DEFINED) {
+                err = KIO::buildErrorString(deleteJob->error(), deleteJob->errorText());
+            } else {
+                err = deleteJob->errorText();
+            }
+
+            setLatestResponseCode(responseCode);
+            setError(UserDefinedError + responseCode);
+            setErrorText(i18n("There was a problem with the request. The item has not been deleted from the server.\n"
+                              "%1 (%2).", err, responseCode));
+        }
+
+        if (hasConflict()) {
+            DavItemFetchJob *fetchJob = new DavItemFetchJob(mUrl, mItem);
+            connect(fetchJob, &DavItemFetchJob::result, this, &DavItemDeleteJob::conflictingItemFetched);
+            fetchJob->start();
+            return;
+        }
+    }
+
+    emitResult();
+}
+
+void DavItemDeleteJob::conflictingItemFetched(KJob *job)
+{
+    DavItemFetchJob *fetchJob = qobject_cast<DavItemFetchJob *>(job);
+    mFreshResponseCode = fetchJob->latestResponseCode();
+
+    if (!job->error()) {
+        mFreshItem = fetchJob->item();
+    }
+
+    emitResult();
+}
+
diff --git a/resources/dav/common/davitemdeletejob.h b/resources/dav/common/davitemdeletejob.h
new file mode 100644 (file)
index 0000000..457d3ef
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVITEMDELETEJOB_H
+#define DAVITEMDELETEJOB_H
+
+#include "davitem.h"
+#include "davjobbase.h"
+#include "davutils.h"
+
+/**
+ * @short A job to delete a DAV item on the DAV server.
+ */
+class DavItemDeleteJob : public DavJobBase
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new dav item delete job.
+     *
+     * @param url The url of the item that shall be deleted.
+     * @param item The item that shall be deleted.
+     * @param parent The parent object.
+     */
+    DavItemDeleteJob(const DavUtils::DavUrl &url, const DavItem &item, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Starts the job.
+     */
+    void start() Q_DECL_OVERRIDE;
+
+    /**
+     * Returns the item that triggered the conflict, if any.
+     */
+    DavItem freshItem() const;
+
+    /**
+     * Returns the response code we got when fetching the fresh item.
+     */
+    int freshResponseCode() const;
+
+private Q_SLOTS:
+    void davJobFinished(KJob *);
+    void conflictingItemFetched(KJob *);
+
+private:
+    DavUtils::DavUrl mUrl;
+    DavItem mItem;
+    DavItem mFreshItem;
+    int mFreshResponseCode;
+};
+
+#endif
diff --git a/resources/dav/common/davitemfetchjob.cpp b/resources/dav/common/davitemfetchjob.cpp
new file mode 100644 (file)
index 0000000..c98088b
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davitemfetchjob.h"
+
+#include "davmanager.h"
+
+#include <kio/davjob.h>
+#include <kio/job.h>
+#include <KLocalizedString>
+
+#include <QtCore/QDebug>
+
+static QString etagFromHeaders(const QString &headers)
+{
+    const QStringList allHeaders = headers.split(QLatin1Char('\n'));
+
+    QString etag;
+    foreach (const QString &header, allHeaders) {
+        if (header.startsWith(QStringLiteral("etag:"), Qt::CaseInsensitive)) {
+            etag = header.section(QLatin1Char(' '), 1);
+        }
+    }
+
+    return etag;
+}
+
+DavItemFetchJob::DavItemFetchJob(const DavUtils::DavUrl &url, const DavItem &item, QObject *parent)
+    : DavJobBase(parent), mUrl(url), mItem(item)
+{
+}
+
+void DavItemFetchJob::start()
+{
+    KIO::StoredTransferJob *job = KIO::storedGet(mUrl.url(), KIO::Reload, KIO::HideProgressInfo | KIO::DefaultFlags);
+    job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
+    // Work around a strange bug in Zimbra (seen at least on CE 5.0.18) : if the user-agent
+    // contains "Mozilla", some strange debug data is displayed in the shared calendars.
+    // This kinda mess up the events parsing...
+    job->addMetaData(QStringLiteral("UserAgent"), QStringLiteral("KDE DAV groupware client"));
+    job->addMetaData(QStringLiteral("cookies"), QStringLiteral("none"));
+    job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
+
+    connect(job, &KIO::StoredTransferJob::result, this, &DavItemFetchJob::davJobFinished);
+}
+
+DavItem DavItemFetchJob::item() const
+{
+    return mItem;
+}
+
+void DavItemFetchJob::davJobFinished(KJob *job)
+{
+    KIO::StoredTransferJob *storedJob = qobject_cast<KIO::StoredTransferJob *>(job);
+    const int responseCode = storedJob->queryMetaData(QStringLiteral("responsecode")).isEmpty() ?
+                             0 :
+                             storedJob->queryMetaData(QStringLiteral("responsecode")).toInt();
+    setLatestResponseCode(responseCode);
+
+    if (storedJob->error()) {
+        QString err;
+        if (storedJob->error() != KIO::ERR_SLAVE_DEFINED) {
+            err = KIO::buildErrorString(storedJob->error(), storedJob->errorText());
+        } else {
+            err = storedJob->errorText();
+        }
+
+        setError(UserDefinedError + responseCode);
+        setErrorText(i18n("There was a problem with the request.\n"
+                          "%1 (%2).", err, responseCode));
+    } else {
+        mItem.setData(storedJob->data());
+        mItem.setContentType(storedJob->queryMetaData(QStringLiteral("content-type")));
+        mItem.setEtag(etagFromHeaders(storedJob->queryMetaData(QStringLiteral("HTTP-Headers"))));
+    }
+
+    emitResult();
+}
+
diff --git a/resources/dav/common/davitemfetchjob.h b/resources/dav/common/davitemfetchjob.h
new file mode 100644 (file)
index 0000000..065cf9b
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVITEMFETCHJOB_H
+#define DAVITEMFETCHJOB_H
+
+#include "davitem.h"
+#include "davjobbase.h"
+#include "davutils.h"
+
+/**
+ * @short A job that fetches a DAV item from the DAV server.
+ */
+class DavItemFetchJob : public DavJobBase
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new dav item fetch job.
+     *
+     * @param url The DAV url of the item that shall be fetched.
+     * @param item The item that shall be fetched.
+     * @param parent The parent object.
+     */
+    DavItemFetchJob(const DavUtils::DavUrl &url, const DavItem &item, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Starts the job.
+     */
+    void start() Q_DECL_OVERRIDE;
+
+    /**
+     * Returns the fetched item including current etag information.
+     */
+    DavItem item() const;
+
+private Q_SLOTS:
+    void davJobFinished(KJob *);
+
+private:
+    DavUtils::DavUrl mUrl;
+    DavItem mItem;
+};
+
+#endif
diff --git a/resources/dav/common/davitemmodifyjob.cpp b/resources/dav/common/davitemmodifyjob.cpp
new file mode 100644 (file)
index 0000000..5b3e4bf
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davitemmodifyjob.h"
+
+#include "davitemfetchjob.h"
+#include "davmanager.h"
+
+#include <kio/job.h>
+#include <KLocalizedString>
+
+DavItemModifyJob::DavItemModifyJob(const DavUtils::DavUrl &url, const DavItem &item, QObject *parent)
+    : DavJobBase(parent), mUrl(url), mItem(item), mFreshResponseCode(0)
+{
+}
+
+void DavItemModifyJob::start()
+{
+    QString headers = QStringLiteral("Content-Type: ");
+    headers += mItem.contentType();
+    headers += QLatin1String("\r\n");
+    headers += QLatin1String("If-Match: ") + mItem.etag();
+
+    KIO::StoredTransferJob *job = KIO::storedPut(mItem.data(), mUrl.url(), -1, KIO::HideProgressInfo | KIO::DefaultFlags);
+    job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
+    job->addMetaData(QStringLiteral("customHTTPHeader"), headers);
+    job->addMetaData(QStringLiteral("cookies"), QStringLiteral("none"));
+    job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
+
+    connect(job, &KIO::StoredTransferJob::result, this, &DavItemModifyJob::davJobFinished);
+}
+
+DavItem DavItemModifyJob::item() const
+{
+    return mItem;
+}
+
+DavItem DavItemModifyJob::freshItem() const
+{
+    return mFreshItem;
+}
+
+int DavItemModifyJob::freshResponseCode() const
+{
+    return mFreshResponseCode;
+}
+
+void DavItemModifyJob::davJobFinished(KJob *job)
+{
+    KIO::StoredTransferJob *storedJob = qobject_cast<KIO::StoredTransferJob *>(job);
+
+    if (storedJob->error()) {
+        const int responseCode = storedJob->queryMetaData(QStringLiteral("responsecode")).isEmpty() ?
+                                 0 :
+                                 storedJob->queryMetaData(QStringLiteral("responsecode")).toInt();
+
+        QString err;
+        if (storedJob->error() != KIO::ERR_SLAVE_DEFINED) {
+            err = KIO::buildErrorString(storedJob->error(), storedJob->errorText());
+        } else {
+            err = storedJob->errorText();
+        }
+
+        setLatestResponseCode(responseCode);
+        setError(UserDefinedError + responseCode);
+        setErrorText(i18n("There was a problem with the request. The item was not modified on the server.\n"
+                          "%1 (%2).", err, responseCode));
+
+        if (hasConflict()) {
+            DavItemFetchJob *fetchJob = new DavItemFetchJob(mUrl, mItem);
+            connect(fetchJob, &DavItemFetchJob::result, this, &DavItemModifyJob::conflictingItemFetched);
+            fetchJob->start();
+        } else {
+            emitResult();
+        }
+
+        return;
+    }
+
+    // The 'Location:' HTTP header is used to indicate the new URL
+    const QStringList allHeaders = storedJob->queryMetaData(QStringLiteral("HTTP-Headers")).split(QLatin1Char('\n'));
+    QString location;
+    foreach (const QString &header, allHeaders) {
+        if (header.startsWith(QLatin1String("location:"), Qt::CaseInsensitive)) {
+            location = header.section(QLatin1Char(' '), 1);
+        }
+    }
+
+    QUrl url;
+    if (location.isEmpty()) {
+        url = storedJob->url();
+    } else if (location.startsWith(QLatin1Char('/'))) {
+        url = storedJob->url();
+        url.setPath(location, QUrl::TolerantMode);
+    } else {
+        url = QUrl::fromUserInput(location);
+    }
+
+    url.setUserInfo(QString());
+    mItem.setUrl(url.toDisplayString());
+
+    DavItemFetchJob *fetchJob = new DavItemFetchJob(mUrl, mItem);
+    connect(fetchJob, &DavItemFetchJob::result, this, &DavItemModifyJob::itemRefreshed);
+    fetchJob->start();
+}
+
+void DavItemModifyJob::itemRefreshed(KJob *job)
+{
+    if (!job->error()) {
+        DavItemFetchJob *fetchJob = qobject_cast<DavItemFetchJob *>(job);
+        mItem.setEtag(fetchJob->item().etag());
+    } else {
+        mItem.setEtag(QString());
+    }
+    emitResult();
+}
+
+void DavItemModifyJob::conflictingItemFetched(KJob *job)
+{
+    DavItemFetchJob *fetchJob = qobject_cast<DavItemFetchJob *>(job);
+    mFreshResponseCode = fetchJob->latestResponseCode();
+
+    if (!job->error()) {
+        mFreshItem = fetchJob->item();
+    }
+
+    emitResult();
+}
+
diff --git a/resources/dav/common/davitemmodifyjob.h b/resources/dav/common/davitemmodifyjob.h
new file mode 100644 (file)
index 0000000..246d35b
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVITEMMODIFYJOB_H
+#define DAVITEMMODIFYJOB_H
+
+#include "davitem.h"
+#include "davjobbase.h"
+#include "davutils.h"
+
+/**
+ * @short A job that modifies a DAV item on the DAV server.
+ */
+class DavItemModifyJob : public DavJobBase
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new dav item modify job.
+     *
+     * @param url The url of the item that shall be modified.
+     * @param item The item that shall be modified.
+     * @param parent The parent object.
+     */
+    DavItemModifyJob(const DavUtils::DavUrl &url, const DavItem &item, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Starts the job.
+     */
+    void start() Q_DECL_OVERRIDE;
+
+    /**
+     * Returns the modified item including the updated etag information.
+     */
+    DavItem item() const;
+
+    /**
+     * Returns the item that triggered the conflict, if any.
+     */
+    DavItem freshItem() const;
+
+    /**
+     * Returns the response code we got when fetching the fresh item.
+     */
+    int freshResponseCode() const;
+
+private Q_SLOTS:
+    void davJobFinished(KJob *);
+    void itemRefreshed(KJob *);
+    void conflictingItemFetched(KJob *);
+
+private:
+    DavUtils::DavUrl mUrl;
+    DavItem mItem;
+    DavItem mFreshItem;
+    int mFreshResponseCode;
+};
+
+#endif
diff --git a/resources/dav/common/davitemsfetchjob.cpp b/resources/dav/common/davitemsfetchjob.cpp
new file mode 100644 (file)
index 0000000..70d3a26
--- /dev/null
@@ -0,0 +1,154 @@
+/*
+    Copyright (c) 2010 Grégory Oestreicher <greg@kamago.net>
+    Based on DavItemsListJob which is copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davitemsfetchjob.h"
+#include "davmanager.h"
+#include "davmultigetprotocol.h"
+#include <AkonadiCore/VectorHelper>
+#include <kio/davjob.h>
+#include <kio/job.h>
+#include <KLocalizedString>
+
+DavItemsFetchJob::DavItemsFetchJob(const DavUtils::DavUrl &collectionUrl, const QStringList &urls, QObject *parent)
+    : KJob(parent), mCollectionUrl(collectionUrl), mUrls(urls)
+{
+}
+
+void DavItemsFetchJob::start()
+{
+    const DavMultigetProtocol *protocol =
+        dynamic_cast<const DavMultigetProtocol *>(DavManager::self()->davProtocol(mCollectionUrl.protocol()));
+    if (!protocol) {
+        setError(UserDefinedError);
+        setErrorText(i18n("Protocol for the collection does not support MULTIGET"));
+        emitResult();
+        return;
+    }
+
+    const QDomDocument report = protocol->itemsReportQuery(mUrls)->buildQuery();
+    KIO::DavJob *job = DavManager::self()->createReportJob(mCollectionUrl.url(), report, QStringLiteral("0"));
+    job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
+    connect(job, &KIO::DavJob::result, this, &DavItemsFetchJob::davJobFinished);
+}
+
+DavItem::List DavItemsFetchJob::items() const
+{
+    return Akonadi::valuesToVector(mItems);
+}
+
+DavItem DavItemsFetchJob::item(const QString &url) const
+{
+    return mItems.value(url);
+}
+
+void DavItemsFetchJob::davJobFinished(KJob *job)
+{
+    KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
+    const int responseCode = davJob->queryMetaData(QStringLiteral("responsecode")).isEmpty() ?
+                             0 :
+                             davJob->queryMetaData(QStringLiteral("responsecode")).toInt();
+
+    // KIO::DavJob does not set error() even if the HTTP status code is a 4xx or a 5xx
+    if (davJob->error() || (responseCode >= 400 && responseCode < 600)) {
+        QString err;
+        if (davJob->error() && davJob->error() != KIO::ERR_SLAVE_DEFINED) {
+            err = KIO::buildErrorString(davJob->error(), davJob->errorText());
+        } else {
+            err = davJob->errorText();
+        }
+
+        setError(UserDefinedError + responseCode);
+        setErrorText(i18n("There was a problem with the request.\n"
+                          "%1 (%2).", err, responseCode));
+
+        emitResult();
+        return;
+    }
+
+    const DavMultigetProtocol *protocol =
+        static_cast<const DavMultigetProtocol *>(DavManager::self()->davProtocol(mCollectionUrl.protocol()));
+
+    const QDomDocument document = davJob->response();
+    const QDomElement documentElement = document.documentElement();
+
+    QDomElement responseElement = DavUtils::firstChildElementNS(documentElement, QStringLiteral("DAV:"), QStringLiteral("response"));
+
+    while (!responseElement.isNull()) {
+        QDomElement propstatElement = DavUtils::firstChildElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("propstat"));
+
+        if (propstatElement.isNull()) {
+            responseElement = DavUtils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
+            continue;
+        }
+
+        // Check for errors
+        const QDomElement statusElement = DavUtils::firstChildElementNS(propstatElement, QStringLiteral("DAV:"), QStringLiteral("status"));
+        if (!statusElement.text().contains(QLatin1String("200"))) {
+            responseElement = DavUtils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
+            continue;
+        }
+
+        const QDomElement propElement = DavUtils::firstChildElementNS(propstatElement, QStringLiteral("DAV:"), QStringLiteral("prop"));
+
+        DavItem item;
+
+        // extract path
+        const QDomElement hrefElement = DavUtils::firstChildElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("href"));
+        const QString href = hrefElement.text();
+
+        QUrl url = davJob->url();
+        url.setUserInfo(QString());
+        if (href.startsWith(QLatin1Char('/'))) {
+            // href is only a path, use request url to complete
+            url.setPath(href, QUrl::TolerantMode);
+        } else {
+            // href is a complete url
+            url = QUrl::fromUserInput(href);
+        }
+
+        item.setUrl(url.toDisplayString());
+
+        // extract etag
+        const QDomElement getetagElement = DavUtils::firstChildElementNS(propElement, QStringLiteral("DAV:"), QStringLiteral("getetag"));
+        item.setEtag(getetagElement.text());
+
+        // extract content
+        const QDomElement dataElement = DavUtils::firstChildElementNS(propElement,
+                                        protocol->responseNamespace(),
+                                        protocol->dataTagName());
+
+        if (dataElement.isNull()) {
+            responseElement = DavUtils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
+            continue;
+        }
+
+        const QByteArray data = dataElement.firstChild().toText().data().toUtf8();
+        if (data.isEmpty()) {
+            responseElement = DavUtils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
+            continue;
+        }
+
+        item.setData(data);
+
+        mItems.insert(item.url(), item);
+        responseElement = DavUtils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
+    }
+
+    emitResult();
+}
diff --git a/resources/dav/common/davitemsfetchjob.h b/resources/dav/common/davitemsfetchjob.h
new file mode 100644 (file)
index 0000000..b7a690e
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+    Copyright (c) 2010 Grégory Oestreicher <greg@kamago.net>
+    Based on DavItemsListJob which is copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVITEMSFETCHJOB_H
+#define DAVITEMSFETCHJOB_H
+
+#include "davitem.h"
+#include "davutils.h"
+
+#include <kjob.h>
+
+#include <QtCore/QMap>
+#include <QtCore/QStringList>
+
+/**
+ * @short A job that fetches a list of items from a DAV server using a multiget query.
+ */
+class DavItemsFetchJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new items fetch job.
+     *
+     * @param collectionUrl The DAV collection on which to run the query
+     * @param urls The list of urls to fetch
+     * @param parent The parent object
+     */
+    DavItemsFetchJob(const DavUtils::DavUrl &collectionUrl, const QStringList &urls, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Starts the job.
+     */
+    void start() Q_DECL_OVERRIDE;
+
+    /**
+     * Returns the list of fetched items
+     */
+    DavItem::List items() const;
+
+    /**
+     * Return the item found at @p url
+     */
+    DavItem item(const QString &url) const;
+
+private Q_SLOTS:
+    void davJobFinished(KJob *);
+
+private:
+    DavUtils::DavUrl mCollectionUrl;
+    QStringList mUrls;
+    QMap<QString, DavItem> mItems;
+};
+
+#endif
diff --git a/resources/dav/common/davitemslistjob.cpp b/resources/dav/common/davitemslistjob.cpp
new file mode 100644 (file)
index 0000000..84c6556
--- /dev/null
@@ -0,0 +1,233 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davitemslistjob.h"
+
+#include "davmanager.h"
+#include "davprotocolbase.h"
+#include "davutils.h"
+#include "etagcache.h"
+
+#include <kio/davjob.h>
+#include <kio/job.h>
+#include <KLocalizedString>
+
+#include <QtCore/QBuffer>
+
+DavItemsListJob::DavItemsListJob(const DavUtils::DavUrl &url, const EtagCache *cache, QObject *parent)
+    : KJob(parent), mUrl(url), mEtagCache(cache), mSubJobCount(0)
+{
+}
+
+void DavItemsListJob::setContentMimeTypes(const QStringList &types)
+{
+    mMimeTypes = types;
+}
+
+void DavItemsListJob::setTimeRange(const QString &start, const QString &end)
+{
+    mRangeStart = start;
+    mRangeEnd = end;
+}
+
+void DavItemsListJob::start()
+{
+    const DavProtocolBase *protocol = DavManager::self()->davProtocol(mUrl.protocol());
+    Q_ASSERT(protocol);
+    QVectorIterator<XMLQueryBuilder::Ptr> it(protocol->itemsQueries());
+
+    while (it.hasNext()) {
+        XMLQueryBuilder::Ptr builder = it.next();
+        if (!mRangeStart.isEmpty()) {
+            builder->setParameter(QStringLiteral("start"), mRangeStart);
+        }
+        if (!mRangeEnd.isEmpty()) {
+            builder->setParameter(QStringLiteral("end"), mRangeEnd);
+        }
+
+        const QDomDocument props = builder->buildQuery();
+        const QString mimeType = builder->mimeType();
+
+        if (mMimeTypes.isEmpty() || mMimeTypes.contains(mimeType)) {
+            ++mSubJobCount;
+            if (protocol->useReport()) {
+                KIO::DavJob *job = DavManager::self()->createReportJob(mUrl.url(), props);
+                job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
+                job->setProperty("davType", QStringLiteral("report"));
+                job->setProperty("itemsMimeType", mimeType);
+                connect(job, &KIO::DavJob::result, this, &DavItemsListJob::davJobFinished);
+            } else {
+                KIO::DavJob *job = DavManager::self()->createPropFindJob(mUrl.url(), props);
+                job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
+                job->setProperty("davType", QStringLiteral("propFind"));
+                job->setProperty("itemsMimeType", mimeType);
+                connect(job, &KIO::DavJob::result, this, &DavItemsListJob::davJobFinished);
+            }
+        }
+    }
+}
+
+DavItem::List DavItemsListJob::items() const
+{
+    return mItems;
+}
+
+DavItem::List DavItemsListJob::changedItems() const
+{
+    return mChangedItems;
+}
+
+QStringList DavItemsListJob::deletedItems() const
+{
+    return mDeletedItems;
+}
+
+void DavItemsListJob::davJobFinished(KJob *job)
+{
+    KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
+    const int responseCode = davJob->queryMetaData(QStringLiteral("responsecode")).isEmpty() ?
+                             0 :
+                             davJob->queryMetaData(QStringLiteral("responsecode")).toInt();
+
+    // KIO::DavJob does not set error() even if the HTTP status code is a 4xx or a 5xx
+    if (davJob->error() || (responseCode >= 400 && responseCode < 600)) {
+        QString err;
+        if (davJob->error() && davJob->error() != KIO::ERR_SLAVE_DEFINED) {
+            err = KIO::buildErrorString(davJob->error(), davJob->errorText());
+        } else {
+            err = davJob->errorText();
+        }
+
+        setError(UserDefinedError + responseCode);
+        setErrorText(i18n("There was a problem with the request.\n"
+                          "%1 (%2).", err, responseCode));
+    } else {
+        /*
+         * Extract data from a document like the following:
+         *
+         * <multistatus xmlns="DAV:">
+         *   <response xmlns="DAV:">
+         *     <href xmlns="DAV:">/caldav.php/test1.user/home/KOrganizer-166749289.780.ics</href>
+         *     <propstat xmlns="DAV:">
+         *       <prop xmlns="DAV:">
+         *         <getetag xmlns="DAV:">"b4bbea0278f4f63854c4167a7656024a"</getetag>
+         *       </prop>
+         *       <status xmlns="DAV:">HTTP/1.1 200 OK</status>
+         *     </propstat>
+         *   </response>
+         *   <response xmlns="DAV:">
+         *     <href xmlns="DAV:">/caldav.php/test1.user/home/KOrganizer-399416366.464.ics</href>
+         *     <propstat xmlns="DAV:">
+         *       <prop xmlns="DAV:">
+         *         <getetag xmlns="DAV:">"52eb129018398a7da4f435b2bc4c6cd5"</getetag>
+         *       </prop>
+         *       <status xmlns="DAV:">HTTP/1.1 200 OK</status>
+         *     </propstat>
+         *   </response>
+         * </multistatus>
+         */
+
+        const QString itemsMimeType = job->property("itemsMimeType").toString();
+        const QDomDocument document = davJob->response();
+        const QDomElement documentElement = document.documentElement();
+
+        QDomElement responseElement = DavUtils::firstChildElementNS(documentElement, QStringLiteral("DAV:"), QStringLiteral("response"));
+        while (!responseElement.isNull()) {
+
+            QDomElement propstatElement;
+
+            // check for the valid propstat, without giving up on first error
+            {
+                const QDomNodeList propstats = responseElement.elementsByTagNameNS(QStringLiteral("DAV:"), QStringLiteral("propstat"));
+                for (int i = 0; i < propstats.length(); ++i) {
+                    const QDomElement propstatCandidate = propstats.item(i).toElement();
+                    const QDomElement statusElement = DavUtils::firstChildElementNS(propstatCandidate, QStringLiteral("DAV:"), QStringLiteral("status"));
+                    if (statusElement.text().contains(QStringLiteral("200"))) {
+                        propstatElement = propstatCandidate;
+                    }
+                }
+            }
+
+            if (propstatElement.isNull()) {
+                responseElement = DavUtils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
+                continue;
+            }
+
+            const QDomElement propElement = DavUtils::firstChildElementNS(propstatElement, QStringLiteral("DAV:"), QStringLiteral("prop"));
+
+            // check whether it is a dav collection ...
+            const QDomElement resourcetypeElement = DavUtils::firstChildElementNS(propElement, QStringLiteral("DAV:"), QStringLiteral("resourcetype"));
+            if (!responseElement.isNull()) {
+                const QDomElement collectionElement = DavUtils::firstChildElementNS(resourcetypeElement, QStringLiteral("DAV:"), QStringLiteral("collection"));
+                if (!collectionElement.isNull()) {
+                    responseElement = DavUtils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
+                    continue;
+                }
+            }
+
+            // ... if not it is an item
+            DavItem item;
+            item.setContentType(itemsMimeType);
+
+            // extract path
+            const QDomElement hrefElement = DavUtils::firstChildElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("href"));
+            const QString href = hrefElement.text();
+
+            QUrl url = davJob->url();
+            url.setUserInfo(QString());
+            if (href.startsWith(QLatin1Char('/'))) {
+                // href is only a path, use request url to complete
+                url.setPath(href, QUrl::TolerantMode);
+            } else {
+                // href is a complete url
+                url = QUrl::fromUserInput(href);
+            }
+
+            QString itemUrl = url.toDisplayString();
+            if (mSeenUrls.contains(itemUrl)) {
+                responseElement = DavUtils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
+                continue;
+            }
+
+            mSeenUrls << itemUrl;
+            item.setUrl(itemUrl);
+
+            // extract etag
+            const QDomElement getetagElement = DavUtils::firstChildElementNS(propElement, QStringLiteral("DAV:"), QStringLiteral("getetag"));
+
+            item.setEtag(getetagElement.text());
+
+            mItems << item;
+
+            if (mEtagCache->etagChanged(itemUrl, item.etag())) {
+                mChangedItems << item;
+            }
+
+            responseElement = DavUtils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
+        }
+    }
+
+    QSet<QString> removed = mEtagCache->urls().toSet();
+    removed.subtract(mSeenUrls);
+    mDeletedItems = removed.toList();
+
+    if (--mSubJobCount == 0) {
+        emitResult();
+    }
+}
+
diff --git a/resources/dav/common/davitemslistjob.h b/resources/dav/common/davitemslistjob.h
new file mode 100644 (file)
index 0000000..5f79140
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVITEMSLISTJOB_H
+#define DAVITEMSLISTJOB_H
+
+#include "davitem.h"
+#include "davutils.h"
+
+#include <kjob.h>
+
+#include <QtCore/QSet>
+#include <QtCore/QStringList>
+
+class EtagCache;
+
+/**
+ * @short A job that lists all DAV items inside a DAV collection.
+ */
+class DavItemsListJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new dav items list job.
+     *
+     * @param url The url of the DAV collection.
+     * @param parent The parent object.
+     */
+    DavItemsListJob(const DavUtils::DavUrl &url, const EtagCache *cache, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Limits the mime types of the items requested.
+     *
+     * If no mime type is given then all will be requested.
+     *
+     * @param types The list of mime types to include
+     */
+    void setContentMimeTypes(const QStringList &types);
+
+    /**
+     * Sets the start and end time to list items for.
+     *
+     * @param start The range start, in format "date with UTC time"
+     * @param end The range end, in format "date with UTC time"
+     */
+    void setTimeRange(const QString &start, const QString &end);
+
+    /**
+     * Starts the job.
+     */
+    void start() Q_DECL_OVERRIDE;
+
+    /**
+     * Returns the list of items seen including identifier url and etag information.
+     */
+    DavItem::List items() const;
+
+    /**
+     * Returns the list of items that were changed on the server.
+     */
+    DavItem::List changedItems() const;
+
+    /**
+     * Returns the list of items URLs that were not seen in the backend.
+     * As this is based on the etag cache this may contain dependent items.
+     */
+    QStringList deletedItems() const;
+
+private Q_SLOTS:
+    void davJobFinished(KJob *);
+
+private:
+    DavUtils::DavUrl mUrl;
+    const EtagCache *mEtagCache;
+    QStringList mMimeTypes;
+    QString mRangeStart;
+    QString mRangeEnd;
+    DavItem::List mItems;
+    QSet<QString> mSeenUrls; // to prevent events duplication with some servers
+    DavItem::List mChangedItems;
+    QStringList mDeletedItems;
+    uint mSubJobCount;
+};
+
+#endif
diff --git a/resources/dav/common/davjobbase.cpp b/resources/dav/common/davjobbase.cpp
new file mode 100644 (file)
index 0000000..6b4a415
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+    Copyright (c) 2014 Gregory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davjobbase.h"
+
+DavJobBase::DavJobBase(QObject *parent)
+    : KJob(parent), mLatestResponseCode(0)
+{
+}
+
+unsigned int DavJobBase::latestResponseCode() const
+{
+    return mLatestResponseCode;
+}
+
+bool DavJobBase::canRetryLater() const
+{
+    bool ret = false;
+
+    // Be explicit and readable by splitting the if/else if clauses
+
+    if (latestResponseCode() == 0 && error()) {
+        // Likely a timeout or a connection failure.
+        ret = true;
+    } else if (latestResponseCode() == 401) {
+        // Authentication required
+        ret = true;
+    } else if (latestResponseCode() == 402) {
+        // Payment required
+        ret = true;
+    } else if (latestResponseCode() == 407) {
+        // Proxy authentication required
+        ret = true;
+    } else if (latestResponseCode() == 408) {
+        // Request timeout
+        ret = true;
+    } else if (latestResponseCode() == 423) {
+        // Locked
+        ret = true;
+    } else if (latestResponseCode() == 429) {
+        // Too many requests
+        ret = true;
+    } else if (latestResponseCode() >= 501 && latestResponseCode() <= 504) {
+        // Various server-side errors
+        ret = true;
+    } else if (latestResponseCode() == 507) {
+        // Insufficient storage
+        ret = true;
+    } else if (latestResponseCode() == 511) {
+        // Network authentication required
+        ret = true;
+    }
+
+    return ret;
+}
+
+bool DavJobBase::hasConflict() const
+{
+    return latestResponseCode() == 412;
+}
+
+void DavJobBase::setLatestResponseCode(unsigned int code)
+{
+    mLatestResponseCode = code;
+}
+
diff --git a/resources/dav/common/davjobbase.h b/resources/dav/common/davjobbase.h
new file mode 100644 (file)
index 0000000..03155d2
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+    Copyright (c) 2014 Gregory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVJOBBASE_H
+#define DAVJOBBASE_H
+
+#include <kjob.h>
+
+/**
+ * @short base class for the jobs used by the resource.
+ */
+class DavJobBase : public KJob
+{
+    Q_OBJECT
+
+public:
+    explicit DavJobBase(QObject *parent = Q_NULLPTR);
+
+    /**
+     * Get the latest response code.
+     *
+     * If no response code has been set then 0 will be returned, but will
+     * be meaningless unless error() is non-zero. In that case this means
+     * that the latest error was not at the HTTP level.
+     */
+    unsigned int latestResponseCode() const;
+
+    /**
+     * Check if the job can be retried later.
+     *
+     * This will return true for transient errors, i.e. if the response code
+     * is either zero and error() is set or if the HTTP response code hints
+     * at a temporary error.
+     *
+     * The HTTP response codes considered retryable are:
+     *   - 401
+     *   - 402
+     *   - 407
+     *   - 408
+     *   - 423
+     *   - 429
+     *   - 501 to 504, inclusive
+     *   - 507
+     *   - 511
+     */
+    bool canRetryLater() const;
+
+    /**
+     * Check if the job failed because of a conflict
+     */
+    bool hasConflict() const;
+
+protected:
+    /**
+     * Sets the latest response code received.
+     *
+     * Only really useful in case of error, though success codes can
+     * also be set.
+     *
+     * @param code The code to set, should be a valid HTTP response code or zero.
+     */
+    void setLatestResponseCode(unsigned int code);
+
+private:
+    unsigned int mLatestResponseCode;
+};
+
+#endif // DAVJOBBASE_H
diff --git a/resources/dav/common/davmanager.cpp b/resources/dav/common/davmanager.cpp
new file mode 100644 (file)
index 0000000..f6ab5f9
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davmanager.h"
+
+#include "caldavprotocol.h"
+#include "carddavprotocol.h"
+#include "groupdavprotocol.h"
+
+#include <kio/davjob.h>
+#include <QDebug>
+
+#include <QtCore/QUrl>
+#include <QtXml/QDomDocument>
+
+DavManager *DavManager::mSelf = Q_NULLPTR;
+
+DavManager::DavManager()
+{
+}
+
+DavManager::~DavManager()
+{
+    QMapIterator<DavUtils::Protocol, DavProtocolBase *> it(mProtocols);
+    while (it.hasNext()) {
+        it.next();
+        delete it.value();
+    }
+}
+
+DavManager *DavManager::self()
+{
+    if (!mSelf) {
+        mSelf = new DavManager();
+    }
+
+    return mSelf;
+}
+
+KIO::DavJob *DavManager::createPropFindJob(const QUrl &url, const QDomDocument &document, const QString &depth) const
+{
+    KIO::DavJob *job = KIO::davPropFind(url, document, depth, KIO::HideProgressInfo | KIO::DefaultFlags);
+
+    // workaround needed, Depth: header doesn't seem to be correctly added
+    const QString header = QLatin1String("Content-Type: text/xml\r\nDepth: ") + depth;
+    job->addMetaData(QStringLiteral("customHTTPHeader"), header);
+    job->addMetaData(QStringLiteral("cookies"), QStringLiteral("none"));
+    job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
+    job->setProperty("extraDavDepth", QVariant::fromValue(depth));
+
+    return job;
+}
+
+KIO::DavJob *DavManager::createReportJob(const QUrl &url, const QDomDocument &document, const QString &depth) const
+{
+    KIO::DavJob *job = KIO::davReport(url, document.toString(), depth, KIO::HideProgressInfo | KIO::DefaultFlags);
+
+    // workaround needed, Depth: header doesn't seem to be correctly added
+    const QString header = QLatin1String("Content-Type: text/xml\r\nDepth: ") + depth;
+    job->addMetaData(QStringLiteral("customHTTPHeader"), header);
+    job->addMetaData(QStringLiteral("cookies"), QStringLiteral("none"));
+    job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
+    job->setProperty("extraDavDepth", QVariant::fromValue(depth));
+
+    return job;
+}
+
+KIO::DavJob *DavManager::createPropPatchJob(const QUrl &url, const QDomDocument &document) const
+{
+    KIO::DavJob *job = KIO::davPropPatch(url, document, KIO::HideProgressInfo | KIO::DefaultFlags);
+    const QString header = QStringLiteral("Content-Type: text/xml");
+    job->addMetaData(QStringLiteral("customHTTPHeader"), header);
+    job->addMetaData(QStringLiteral("cookies"), QStringLiteral("none"));
+    job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
+    return job;
+}
+
+const DavProtocolBase *DavManager::davProtocol(DavUtils::Protocol protocol)
+{
+    if (createProtocol(protocol)) {
+        return mProtocols[ protocol ];
+    } else {
+        return Q_NULLPTR;
+    }
+}
+
+bool DavManager::createProtocol(DavUtils::Protocol protocol)
+{
+    if (mProtocols.contains(protocol)) {
+        return true;
+    }
+
+    switch (protocol) {
+    case DavUtils::CalDav:
+        mProtocols.insert(DavUtils::CalDav, new CaldavProtocol());
+        break;
+    case DavUtils::CardDav:
+        mProtocols.insert(DavUtils::CardDav, new CarddavProtocol());
+        break;
+    case DavUtils::GroupDav:
+        mProtocols.insert(DavUtils::GroupDav, new GroupdavProtocol());
+        break;
+    default:
+        qCritical() << "Unknown protocol: " << static_cast<int>(protocol);
+        return false;
+    }
+
+    return true;
+}
diff --git a/resources/dav/common/davmanager.h b/resources/dav/common/davmanager.h
new file mode 100644 (file)
index 0000000..7db5802
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVMANAGER_H
+#define DAVMANAGER_H
+
+#include "davutils.h"
+
+#include <QtCore/QMap>
+#include <QtCore/QString>
+
+class DavProtocolBase;
+
+namespace KIO
+{
+class DavJob;
+}
+
+class QUrl;
+
+class QDomDocument;
+
+/**
+ * @short A factory class for handling DAV jobs.
+ *
+ * This class provides factory methods to create preconfigured
+ * low-level DAV jobs and has access to the global DAV protocol dialect
+ * objects which abstract the access to the various DAV protocol dialects.
+ */
+class DavManager
+{
+public:
+    /**
+     * Destroys the DAV manager.
+     */
+    ~DavManager();
+
+    /**
+     * Returns the global instance of the DAV manager.
+     */
+    static DavManager *self();
+
+    /**
+     * Returns a preconfigured DAV PROPFIND job.
+     *
+     * @param url The target url of the job.
+     * @param document The query XML document.
+     * @param depth The Depth: value to send in the HTTP request
+     */
+    KIO::DavJob *createPropFindJob(const QUrl &url, const QDomDocument &document, const QString &depth = QStringLiteral("1")) const;
+
+    /**
+     * Returns a preconfigured DAV REPORT job.
+     *
+     * @param url The target url of the job.
+     * @param document The query XML document.
+     * @param depth The Depth: value to send in the HTTP request
+     */
+    KIO::DavJob *createReportJob(const QUrl &url, const QDomDocument &document, const QString &depth = QStringLiteral("1")) const;
+
+    /**
+     * Returns a preconfigured DAV PROPPATCH job.
+     *
+     * @param url The target url of the job.
+     * @param document The query XML document.
+     */
+    KIO::DavJob *createPropPatchJob(const QUrl &url, const QDomDocument &document) const;
+
+    /**
+     * Returns the DAV protocol dialect object for the given DAV @p protocol.
+     */
+    const DavProtocolBase *davProtocol(DavUtils::Protocol protocol);
+
+private:
+    /**
+     * Creates a new DAV manager.
+     */
+    DavManager();
+
+    /**
+     * Creates a new protocol.
+     */
+    bool createProtocol(DavUtils::Protocol protocol);
+
+    typedef QMap<DavUtils::Protocol, DavProtocolBase *> protocolsMap;
+    protocolsMap mProtocols;
+    static DavManager *mSelf;
+};
+
+#endif
diff --git a/resources/dav/common/davmultigetprotocol.cpp b/resources/dav/common/davmultigetprotocol.cpp
new file mode 100644 (file)
index 0000000..0d1e385
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+    Copyright (c) 2010 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davmultigetprotocol.h"
+
+DavMultigetProtocol::~DavMultigetProtocol()
+{
+}
diff --git a/resources/dav/common/davmultigetprotocol.h b/resources/dav/common/davmultigetprotocol.h
new file mode 100644 (file)
index 0000000..b857878
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+    Copyright (c) 2010 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVMULTIGETPROTOCOL_H
+#define DAVMULTIGETPROTOCOL_H
+
+#include "davprotocolbase.h"
+
+/**
+ * @short Base class for protocols that implement multiget capabilities
+ */
+class DavMultigetProtocol : public DavProtocolBase
+{
+public:
+    /**
+     * Destroys the DAV protocol
+     */
+    virtual ~DavMultigetProtocol();
+
+    /**
+     * Returns the XML document that represents a MULTIGET DAV query to
+     * list all DAV resources with the given @p urls.
+     */
+    virtual XMLQueryBuilder::Ptr itemsReportQuery(const QStringList &urls) const = 0;
+
+    /**
+     * Returns the namespace used by protocol-specific elements found in responses.
+     */
+    virtual QString responseNamespace() const = 0;
+
+    /**
+     * Returns the tag name of data elements found in responses.
+     */
+    virtual QString dataTagName() const = 0;
+};
+
+#endif
diff --git a/resources/dav/common/davprincipalhomesetsfetchjob.cpp b/resources/dav/common/davprincipalhomesetsfetchjob.cpp
new file mode 100644 (file)
index 0000000..1383efd
--- /dev/null
@@ -0,0 +1,223 @@
+/*
+    Copyright (c) 2010 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davprincipalhomesetsfetchjob.h"
+
+#include "davmanager.h"
+#include "davprotocolbase.h"
+
+#include <kio/davjob.h>
+#include <kio/job.h>
+#include <KLocalizedString>
+
+DavPrincipalHomeSetsFetchJob::DavPrincipalHomeSetsFetchJob(const DavUtils::DavUrl &url, QObject *parent)
+    : DavJobBase(parent), mUrl(url)
+{
+}
+
+void DavPrincipalHomeSetsFetchJob::start()
+{
+    fetchHomeSets(false);
+}
+
+void DavPrincipalHomeSetsFetchJob::fetchHomeSets(bool homeSetsOnly)
+{
+    QDomDocument document;
+
+    QDomElement propfindElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("propfind"));
+    document.appendChild(propfindElement);
+
+    QDomElement propElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
+    propfindElement.appendChild(propElement);
+
+    const QString homeSet = DavManager::self()->davProtocol(mUrl.protocol())->principalHomeSet();
+    const QString homeSetNS = DavManager::self()->davProtocol(mUrl.protocol())->principalHomeSetNS();
+    propElement.appendChild(document.createElementNS(homeSetNS, homeSet));
+
+    if (!homeSetsOnly) {
+        propElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("current-user-principal")));
+        propElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("principal-URL")));
+    }
+
+    KIO::DavJob *job = DavManager::self()->createPropFindJob(mUrl.url(), document, QStringLiteral("0"));
+    job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
+    connect(job, &KIO::DavJob::result, this, &DavPrincipalHomeSetsFetchJob::davJobFinished);
+}
+
+QStringList DavPrincipalHomeSetsFetchJob::homeSets() const
+{
+    return mHomeSets;
+}
+
+void DavPrincipalHomeSetsFetchJob::davJobFinished(KJob *job)
+{
+    KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
+    const int responseCode = davJob->queryMetaData(QStringLiteral("responsecode")).isEmpty() ?
+                             0 :
+                             davJob->queryMetaData(QStringLiteral("responsecode")).toInt();
+
+    // KIO::DavJob does not set error() even if the HTTP status code is a 4xx or a 5xx
+    if (davJob->error() || (responseCode >= 400 && responseCode < 600)) {
+        QString err;
+        if (davJob->error() && davJob->error() != KIO::ERR_SLAVE_DEFINED) {
+            err = KIO::buildErrorString(davJob->error(), davJob->errorText());
+        } else {
+            err = davJob->errorText();
+        }
+
+        setLatestResponseCode(responseCode);
+        setError(UserDefinedError + responseCode);
+        setErrorText(i18n("There was a problem with the request.\n"
+                          "%1 (%2).", err, responseCode));
+
+        emitResult();
+        return;
+    }
+
+    /*
+     * Extract information from a document like the following (if no homeset is defined) :
+     *
+     * <D:multistatus xmlns:D="DAV:">
+     *  <D:response xmlns:D="DAV:">
+     *   <D:href xmlns:D="DAV:">/dav/</D:href>
+     *   <D:propstat xmlns:D="DAV:">
+     *    <D:status xmlns:D="DAV:">HTTP/1.1 200 OK</D:status>
+     *    <D:prop xmlns:D="DAV:">
+     *     <D:current-user-principal xmlns:D="DAV:">
+     *      <D:href xmlns:D="DAV:">/principals/users/gdacoin/</D:href>
+     *     </D:current-user-principal>
+     *    </D:prop>
+     *   </D:propstat>
+     *   <D:propstat xmlns:D="DAV:">
+     *    <D:status xmlns:D="DAV:">HTTP/1.1 404 Not Found</D:status>
+     *    <D:prop xmlns:D="DAV:">
+     *     <principal-URL xmlns="DAV:"/>
+     *     <calendar-home-set xmlns="urn:ietf:params:xml:ns:caldav"/>
+     *    </D:prop>
+     *   </D:propstat>
+     *  </D:response>
+     * </D:multistatus>
+     *
+     * Or like this (if the homeset is defined):
+     *
+     *  <?xml version="1.0" encoding="utf-8" ?>
+     *  <multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
+     *    <response>
+     *      <href>/principals/users/greg%40kamago.net/</href>
+     *      <propstat>
+     *        <prop>
+     *          <C:calendar-home-set>
+     *            <href>/greg%40kamago.net/</href>
+     *          </C:calendar-home-set>
+     *        </prop>
+     *        <status>HTTP/1.1 200 OK</status>
+     *      </propstat>
+     *    </response>
+     *  </multistatus>
+     */
+
+    const QString homeSet = DavManager::self()->davProtocol(mUrl.protocol())->principalHomeSet();
+    const QString homeSetNS = DavManager::self()->davProtocol(mUrl.protocol())->principalHomeSetNS();
+    QString nextRoundHref; // The content of the href element that will be used if no homeset was found.
+    // This is either given by current-user-principal or by principal-URL.
+
+    const QDomDocument document = davJob->response();
+    const QDomElement multistatusElement = document.documentElement();
+
+    QDomElement responseElement = DavUtils::firstChildElementNS(multistatusElement, QStringLiteral("DAV:"), QStringLiteral("response"));
+    while (!responseElement.isNull()) {
+
+        QDomElement propstatElement;
+
+        // check for the valid propstat, without giving up on first error
+        {
+            const QDomNodeList propstats = responseElement.elementsByTagNameNS(QStringLiteral("DAV:"), QStringLiteral("propstat"));
+            for (int i = 0; i < propstats.length(); ++i) {
+                const QDomElement propstatCandidate = propstats.item(i).toElement();
+                const QDomElement statusElement = DavUtils::firstChildElementNS(propstatCandidate, QStringLiteral("DAV:"), QStringLiteral("status"));
+                if (statusElement.text().contains(QLatin1String("200"))) {
+                    propstatElement = propstatCandidate;
+                }
+            }
+        }
+
+        if (propstatElement.isNull()) {
+            responseElement = DavUtils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
+            continue;
+        }
+
+        // extract home sets
+        const QDomElement propElement = DavUtils::firstChildElementNS(propstatElement, QStringLiteral("DAV:"), QStringLiteral("prop"));
+        const QDomElement homeSetElement = DavUtils::firstChildElementNS(propElement, homeSetNS, homeSet);
+
+        if (!homeSetElement.isNull()) {
+            QDomElement hrefElement = DavUtils::firstChildElementNS(homeSetElement, QStringLiteral("DAV:"), QStringLiteral("href"));
+
+            while (!hrefElement.isNull()) {
+                const QString href = hrefElement.text();
+                if (!mHomeSets.contains(href)) {
+                    mHomeSets << href;
+                }
+
+                hrefElement = DavUtils::nextSiblingElementNS(hrefElement, QStringLiteral("DAV:"), QStringLiteral("href"));
+            }
+        } else {
+            // Trying to get the principal url, given either by current-user-principal or principal-URL
+            QDomElement urlHolder = DavUtils::firstChildElementNS(propElement, QStringLiteral("DAV:"), QStringLiteral("current-user-principal"));
+            if (urlHolder.isNull()) {
+                urlHolder = DavUtils::firstChildElementNS(propElement, QStringLiteral("DAV:"), QStringLiteral("principal-URL"));
+            }
+
+            if (!urlHolder.isNull()) {
+                // Getting the href that will be used for the next round
+                QDomElement hrefElement = DavUtils::firstChildElementNS(urlHolder, QStringLiteral("DAV:"), QStringLiteral("href"));
+                if (!hrefElement.isNull()) {
+                    nextRoundHref = hrefElement.text();
+                }
+            }
+        }
+
+        responseElement = DavUtils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
+    }
+
+    /*
+     * Now either we got one or more homesets, or we got an href for the next round
+     * or nothing can be found by this job.
+     * If we have homesets, we're done here and can notify the caller.
+     * Else we must ensure that we have an href for the next round.
+     */
+    if (!mHomeSets.isEmpty() || nextRoundHref.isEmpty()) {
+        emitResult();
+    } else {
+        QUrl nextRoundUrl(mUrl.url());
+
+        if (nextRoundHref.startsWith(QLatin1Char('/'))) {
+            // nextRoundHref is only a path, use request url to complete
+            nextRoundUrl.setPath(nextRoundHref, QUrl::TolerantMode);
+        } else {
+            // href is a complete url
+            nextRoundUrl = QUrl::fromUserInput(nextRoundHref);
+            nextRoundUrl.setUserName(mUrl.url().userName());
+            nextRoundUrl.setPassword(mUrl.url().password());
+        }
+
+        mUrl.setUrl(nextRoundUrl);
+        // And one more round, fetching only homesets
+        fetchHomeSets(true);
+    }
+}
diff --git a/resources/dav/common/davprincipalhomesetsfetchjob.h b/resources/dav/common/davprincipalhomesetsfetchjob.h
new file mode 100644 (file)
index 0000000..6d05b87
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+    Copyright (c) 2010 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVPRINCIPALHOMESETSFETCHJOB_H
+#define DAVPRINCIPALHOMESETSFETCHJOB_H
+
+#include "davjobbase.h"
+#include "davutils.h"
+
+#include <kjob.h>
+
+#include <QtCore/QStringList>
+
+/**
+ * @short A job that fetches home sets for a principal.
+ */
+class DavPrincipalHomeSetsFetchJob : public DavJobBase
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new dav principals home sets fetch job.
+     *
+     * @param url The DAV url of the DAV principal.
+     * @param parent The parent object.
+     */
+    explicit DavPrincipalHomeSetsFetchJob(const DavUtils::DavUrl &url, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Starts the job.
+     */
+    void start() Q_DECL_OVERRIDE;
+
+    /**
+     * Returns the found home sets.
+     */
+    QStringList homeSets() const;
+
+private Q_SLOTS:
+    void davJobFinished(KJob *);
+
+private:
+    /**
+     * Start the fetch process.
+     *
+     * There may be two rounds necessary if the first request
+     * does not returns the home sets, but only the current-user-principal
+     * or the principal-URL. The bool flag is here to prevent requesting
+     * those last two on each request, as they are only fetched in
+     * the first round.
+     *
+     * @param fetchHomeSetsOnly If set to true the request will not include
+     *        the current-user-principal and principal-URL props.
+     */
+    void fetchHomeSets(bool fetchHomeSetsOnly);
+
+    DavUtils::DavUrl mUrl;
+    QStringList mHomeSets;
+};
+
+#endif
diff --git a/resources/dav/common/davprincipalsearchjob.cpp b/resources/dav/common/davprincipalsearchjob.cpp
new file mode 100644 (file)
index 0000000..be7453a
--- /dev/null
@@ -0,0 +1,381 @@
+/*
+    Copyright (c) 2011 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davprincipalsearchjob.h"
+
+#include "davmanager.h"
+
+#include <kio/job.h>
+#include <kio/davjob.h>
+#include <KLocalizedString>
+
+#include <QtCore/QUrl>
+
+DavPrincipalSearchJob::DavPrincipalSearchJob(const DavUtils::DavUrl &url, DavPrincipalSearchJob::FilterType type,
+        const QString &filter, QObject *parent)
+    : KJob(parent), mUrl(url), mType(type), mFilter(filter), mPrincipalPropertySearchSubJobCount(0),
+      mPrincipalPropertySearchSubJobSuccessful(false)
+{
+}
+
+void DavPrincipalSearchJob::fetchProperty(const QString &name, const QString &ns)
+{
+    QString propNamespace = ns;
+    if (propNamespace.isEmpty()) {
+        propNamespace = QStringLiteral("DAV:");
+    }
+
+    mFetchProperties << QPair<QString, QString>(propNamespace, name);
+}
+
+DavUtils::DavUrl DavPrincipalSearchJob::davUrl() const
+{
+    return mUrl;
+}
+
+void DavPrincipalSearchJob::start()
+{
+    /*
+     * The first step is to try to locate the URL that contains the principals.
+     * This is done with a PROPFIND request and a XML like this:
+     * <?xml version="1.0" encoding="utf-8" ?>
+     * <D:propfind xmlns:D="DAV:">
+     *   <D:prop>
+     *     <D:principal-collection-set/>
+     *   </D:prop>
+     * </D:propfind>
+     */
+    QDomDocument query;
+
+    QDomElement propfind = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("propfind"));
+    query.appendChild(propfind);
+
+    QDomElement prop = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
+    propfind.appendChild(prop);
+
+    QDomElement principalCollectionSet = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("principal-collection-set"));
+    prop.appendChild(principalCollectionSet);
+
+    KIO::DavJob *job = DavManager::self()->createPropFindJob(mUrl.url(), query);
+    job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
+    connect(job, &KIO::DavJob::result, this, &DavPrincipalSearchJob::principalCollectionSetSearchFinished);
+    job->start();
+}
+
+void DavPrincipalSearchJob::principalCollectionSetSearchFinished(KJob *job)
+{
+    KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
+    const int responseCode = davJob->queryMetaData(QStringLiteral("responsecode")).isEmpty() ?
+                             0 :
+                             davJob->queryMetaData(QStringLiteral("responsecode")).toInt();
+
+    // KIO::DavJob does not set error() even if the HTTP status code is a 4xx or a 5xx
+    if (davJob->error() || (responseCode >= 400 && responseCode < 600)) {
+        QString err;
+        if (davJob->error() && davJob->error() != KIO::ERR_SLAVE_DEFINED) {
+            err = KIO::buildErrorString(davJob->error(), davJob->errorText());
+        } else {
+            err = davJob->errorText();
+        }
+
+        setError(UserDefinedError + responseCode);
+        setErrorText(i18n("There was a problem with the request.\n"
+                          "%1 (%2).", err, responseCode));
+
+        emitResult();
+        return;
+    }
+
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+        emitResult();
+        return;
+    }
+
+    /*
+     * Extract information from a document like the following:
+     *
+     * <?xml version="1.0" encoding="utf-8" ?>
+     * <D:multistatus xmlns:D="DAV:">
+     *   <D:response>
+     *     <D:href>http://www.example.com/papers/</D:href>
+     *     <D:propstat>
+     *       <D:prop>
+     *         <D:principal-collection-set>
+     *           <D:href>http://www.example.com/acl/users/</D:href>
+     *           <D:href>http://www.example.com/acl/groups/</D:href>
+     *         </D:principal-collection-set>
+     *       </D:prop>
+     *       <D:status>HTTP/1.1 200 OK</D:status>
+     *     </D:propstat>
+     *   </D:response>
+     * </D:multistatus>
+     */
+
+    QDomDocument document = davJob->response();
+    QDomElement documentElement = document.documentElement();
+
+    QDomElement responseElement = DavUtils::firstChildElementNS(documentElement, QStringLiteral("DAV:"), QStringLiteral("response"));
+    if (responseElement.isNull()) {
+        emitResult();
+        return;
+    }
+
+    // check for the valid propstat, without giving up on first error
+    QDomElement propstatElement;
+    {
+        const QDomNodeList propstats = responseElement.elementsByTagNameNS(QStringLiteral("DAV:"), QStringLiteral("propstat"));
+        for (int i = 0; i < propstats.length(); ++i) {
+            const QDomElement propstatCandidate = propstats.item(i).toElement();
+            const QDomElement statusElement = DavUtils::firstChildElementNS(propstatCandidate, QStringLiteral("DAV:"), QStringLiteral("status"));
+            if (statusElement.text().contains(QStringLiteral("200"))) {
+                propstatElement = propstatCandidate;
+            }
+        }
+    }
+
+    if (propstatElement.isNull()) {
+        emitResult();
+        return;
+    }
+
+    QDomElement propElement = DavUtils::firstChildElementNS(propstatElement, QStringLiteral("DAV:"), QStringLiteral("prop"));
+    if (propElement.isNull()) {
+        emitResult();
+        return;
+    }
+
+    QDomElement principalCollectionSetElement = DavUtils::firstChildElementNS(propElement, QStringLiteral("DAV:"), QStringLiteral("principal-collection-set"));
+    if (principalCollectionSetElement.isNull()) {
+        emitResult();
+        return;
+    }
+
+    QDomNodeList hrefNodes = principalCollectionSetElement.elementsByTagNameNS(QStringLiteral("DAV:"), QStringLiteral("href"));
+    for (int i = 0; i < hrefNodes.size(); ++i) {
+        QDomElement hrefElement = hrefNodes.at(i).toElement();
+        QString href = hrefElement.text();
+
+        QUrl url = mUrl.url();
+        if (href.startsWith(QLatin1Char('/'))) {
+            // href is only a path, use request url to complete
+            url.setPath(href, QUrl::TolerantMode);
+        } else {
+            // href is a complete url
+            QUrl tmpUrl(href);
+            tmpUrl.setUserName(url.userName());
+            tmpUrl.setPassword(url.password());
+            url = tmpUrl;
+        }
+
+        QDomDocument principalPropertySearchQuery;
+        buildReportQuery(principalPropertySearchQuery);
+        KIO::DavJob *reportJob = DavManager::self()->createReportJob(url, principalPropertySearchQuery);
+        reportJob->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
+        connect(reportJob, &KIO::DavJob::result, this, &DavPrincipalSearchJob::principalPropertySearchFinished);
+        ++mPrincipalPropertySearchSubJobCount;
+        reportJob->start();
+    }
+}
+
+void DavPrincipalSearchJob::principalPropertySearchFinished(KJob *job)
+{
+    --mPrincipalPropertySearchSubJobCount;
+
+    if (job->error() && !mPrincipalPropertySearchSubJobSuccessful) {
+        setError(job->error());
+        setErrorText(job->errorText());
+        if (mPrincipalPropertySearchSubJobCount == 0) {
+            emitResult();
+        }
+        return;
+    }
+
+    KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
+
+    const int responseCode = davJob->queryMetaData(QStringLiteral("responsecode")).toInt();
+
+    if (responseCode > 499 && responseCode < 600 && !mPrincipalPropertySearchSubJobSuccessful) {
+        // Server-side error, unrecoverable
+        setError(UserDefinedError);
+        setErrorText(i18n("The server encountered an error that prevented it from completing your request"));
+        if (mPrincipalPropertySearchSubJobCount == 0) {
+            emitResult();
+        }
+        return;
+    } else if (responseCode > 399 && responseCode < 500 && !mPrincipalPropertySearchSubJobSuccessful) {
+        // User-side error
+        QString extraMessage;
+        if (responseCode == 401) {
+            extraMessage = i18n("Invalid username/password");
+        } else if (responseCode == 403) {
+            extraMessage = i18n("Access forbidden");
+        } else if (responseCode == 404) {
+            extraMessage = i18n("Resource not found");
+        } else {
+            extraMessage = i18n("HTTP error");
+        }
+
+        setError(UserDefinedError);
+        setErrorText(i18n("There was a problem with the request.\n"
+                          "%1 (%2).", extraMessage, responseCode));
+        if (mPrincipalPropertySearchSubJobCount == 0) {
+            emitResult();
+        }
+        return;
+    }
+
+    if (!mPrincipalPropertySearchSubJobSuccessful) {
+        setError(0);   // nope, everything went fine
+        mPrincipalPropertySearchSubJobSuccessful = true;
+    }
+
+    /*
+     * Extract infos from a document like the following:
+     * <?xml version="1.0" encoding="utf-8" ?>
+     * <D:multistatus xmlns:D="DAV:" xmlns:B="http://BigCorp.com/ns/">
+     *   <D:response>
+     *     <D:href>http://www.example.com/users/jdoe</D:href>
+     *     <D:propstat>
+     *       <D:prop>
+     *         <D:displayname>John Doe</D:displayname>
+     *       </D:prop>
+     *       <D:status>HTTP/1.1 200 OK</D:status>
+     *     </D:propstat>
+     * </D:multistatus>
+    */
+
+    const QDomDocument document = davJob->response();
+    const QDomElement documentElement = document.documentElement();
+
+    QDomElement responseElement = DavUtils::firstChildElementNS(documentElement, QStringLiteral("DAV:"), QStringLiteral("response"));
+    if (responseElement.isNull()) {
+        if (mPrincipalPropertySearchSubJobCount == 0) {
+            emitResult();
+        }
+        return;
+    }
+
+    // check for the valid propstat, without giving up on first error
+    QDomElement propstatElement;
+    {
+        const QDomNodeList propstats = responseElement.elementsByTagNameNS(QStringLiteral("DAV:"), QStringLiteral("propstat"));
+        const int propStatsEnd(propstats.length());
+        for (int i = 0; i < propStatsEnd; ++i) {
+            const QDomElement propstatCandidate = propstats.item(i).toElement();
+            const QDomElement statusElement = DavUtils::firstChildElementNS(propstatCandidate, QStringLiteral("DAV:"), QStringLiteral("status"));
+            if (statusElement.text().contains(QStringLiteral("200"))) {
+                propstatElement = propstatCandidate;
+            }
+        }
+    }
+
+    if (propstatElement.isNull()) {
+        if (mPrincipalPropertySearchSubJobCount == 0) {
+            emitResult();
+        }
+        return;
+    }
+
+    QDomElement propElement = DavUtils::firstChildElementNS(propstatElement, QStringLiteral("DAV:"), QStringLiteral("prop"));
+    if (propElement.isNull()) {
+        if (mPrincipalPropertySearchSubJobCount == 0) {
+            emitResult();
+        }
+        return;
+    }
+
+    // All requested properties are now under propElement, so let's find them
+    typedef QPair<QString, QString> PropertyPair;
+    foreach (const PropertyPair &fetchProperty, mFetchProperties) {
+        QDomNodeList fetchNodes = propElement.elementsByTagNameNS(fetchProperty.first, fetchProperty.second);
+        for (int i = 0; i < fetchNodes.size(); ++i) {
+            QDomElement fetchElement = fetchNodes.at(i).toElement();
+            Result result;
+            result.propertyNamespace = fetchProperty.first;
+            result.property = fetchProperty.second;
+            result.value = fetchElement.text();
+            mResults << result;
+        }
+    }
+
+    if (mPrincipalPropertySearchSubJobCount == 0) {
+        emitResult();
+    }
+}
+
+QList< DavPrincipalSearchJob::Result > DavPrincipalSearchJob::results() const
+{
+    return mResults;
+}
+
+void DavPrincipalSearchJob::buildReportQuery(QDomDocument &query)
+{
+    /*
+     * Build a document like the following, where XXX will
+     * be replaced by the properties the user want to fetch:
+     *
+     *  <?xml version="1.0" encoding="utf-8" ?>
+     *  <D:principal-property-search xmlns:D="DAV:">
+     *    <D:property-search>
+     *      <D:prop>
+     *        <D:displayname/>
+     *      </D:prop>
+     *      <D:match>FILTER</D:match>
+     *    </D:property-search>
+     *    <D:prop>
+     *      XXX
+     *    </D:prop>
+     *  </D:principal-property-search>
+     */
+
+    QDomElement principalPropertySearch = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("principal-property-search"));
+    query.appendChild(principalPropertySearch);
+
+    QDomElement propertySearch = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("property-search"));
+    principalPropertySearch.appendChild(propertySearch);
+
+    QDomElement prop = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
+    propertySearch.appendChild(prop);
+
+    if (mType == DisplayName) {
+        QDomElement displayName = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("displayname"));
+        prop.appendChild(displayName);
+    } else if (mType == EmailAddress) {
+        QDomElement calendarUserAddressSet = query.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("calendar-user-address-set"));
+        prop.appendChild(calendarUserAddressSet);
+        //QDomElement hrefElement = query.createElementNS( "DAV:", "href" );
+        //prop.appendChild( hrefElement );
+    }
+
+    QDomElement match = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("match"));
+    propertySearch.appendChild(match);
+
+    QDomText propFilter = query.createTextNode(mFilter);
+    match.appendChild(propFilter);
+
+    prop = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
+    principalPropertySearch.appendChild(prop);
+
+    typedef QPair<QString, QString> PropertyPair;
+    foreach (const PropertyPair &fetchProperty, mFetchProperties) {
+        QDomElement elem = query.createElementNS(fetchProperty.first, fetchProperty.second);
+        prop.appendChild(elem);
+    }
+}
diff --git a/resources/dav/common/davprincipalsearchjob.h b/resources/dav/common/davprincipalsearchjob.h
new file mode 100644 (file)
index 0000000..625a783
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+    Copyright (c) 2011 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVPRINCIPALSEARCHJOB_H
+#define DAVPRINCIPALSEARCHJOB_H
+
+#include "davutils.h"
+
+#include <QtCore/QList>
+#include <QtCore/QPair>
+#include <QtCore/QString>
+
+#include <kjob.h>
+
+/**
+ * @short A job that search a DAV principal on a server
+ *
+ * This job is used to search a principal on a server
+ * that implement the dav-property-search REPORT (RFC3744).
+ *
+ * The properties to fetch are set with @ref fetchProperty().
+ */
+class DavPrincipalSearchJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Types of search that are supported by this job.
+     * DisplayName will match on the DAV displayname property.
+     * EmailAddress will match on the CalDav calendar-user-address-set property.
+     */
+    enum FilterType {
+        DisplayName,
+        EmailAddress
+    };
+
+    /**
+     * Simple struct to hold the search job results
+     */
+    struct Result {
+        QString propertyNamespace;
+        QString property;
+        QString value;
+    };
+
+    /**
+     * Creates a new dav principal search job
+     *
+     * @param url The URL to use in the REPORT query.
+     * @param type The type that the filter will match.
+     * @param filter The filter that will be used to match the displayname attribute.
+     * @param parent The parent object.
+     */
+    explicit DavPrincipalSearchJob(const DavUtils::DavUrl &url, FilterType type, const QString &filter, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Add a new property to fetch from the server.
+     *
+     * @param name The name of the property.
+     * @param ns The namespace of this property, defaults to 'DAV:'.
+     */
+    void fetchProperty(const QString &name, const QString &ns = QString());
+
+    /**
+     * Starts the job
+     */
+    void start() Q_DECL_OVERRIDE;
+
+    /**
+     * Return the DavUrl used by this job
+     */
+    DavUtils::DavUrl davUrl() const;
+
+    /**
+     * Get the job results.
+     */
+    QList<Result> results() const;
+
+private Q_SLOTS:
+    void principalCollectionSetSearchFinished(KJob *job);
+    void principalPropertySearchFinished(KJob *job);
+
+private:
+    void buildReportQuery(QDomDocument &query);
+
+private:
+    DavUtils::DavUrl mUrl;
+    FilterType mType;
+    QString mFilter;
+    int mPrincipalPropertySearchSubJobCount;
+    bool mPrincipalPropertySearchSubJobSuccessful;
+    QList< QPair<QString, QString> > mFetchProperties;
+    QList<Result> mResults;
+};
+
+#endif // DAVPRINCIPALSEARCHJOB_H
diff --git a/resources/dav/common/davprotocolbase.cpp b/resources/dav/common/davprotocolbase.cpp
new file mode 100644 (file)
index 0000000..7241fd2
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+    Copyright (c) 2009 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davprotocolbase.h"
+
+XMLQueryBuilder::~XMLQueryBuilder()
+{
+}
+
+void XMLQueryBuilder::setParameter(const QString &key, const QVariant &value)
+{
+    mParameters[key] = value;
+}
+
+QVariant XMLQueryBuilder::parameter(const QString &key) const
+{
+    QVariant ret;
+    if (mParameters.contains(key)) {
+        ret = mParameters.value(key);
+    }
+    return ret;
+}
+
+DavProtocolBase::~DavProtocolBase()
+{
+}
+
+QString DavProtocolBase::principalHomeSet() const
+{
+    return QString();
+}
+
+QString DavProtocolBase::principalHomeSetNS() const
+{
+    return QString();
+}
diff --git a/resources/dav/common/davprotocolbase.h b/resources/dav/common/davprotocolbase.h
new file mode 100644 (file)
index 0000000..b4d6c72
--- /dev/null
@@ -0,0 +1,130 @@
+/*
+    Copyright (c) 2009 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVPROTOCOLBASE_H
+#define DAVPROTOCOLBASE_H
+
+#include "davcollection.h"
+
+#include <QtCore/QList>
+#include <QtCore/QMap>
+#include <QtXml/QDomDocument>
+
+/**
+ * @short Base class for XML query builders
+ */
+class XMLQueryBuilder
+{
+public:
+    typedef QSharedPointer<XMLQueryBuilder> Ptr;
+
+    virtual ~XMLQueryBuilder();
+
+    virtual QDomDocument buildQuery() const = 0;
+    virtual QString mimeType() const = 0;
+
+    void setParameter(const QString &key, const QVariant &value);
+    QVariant parameter(const QString &key) const;
+
+private:
+    QMap<QString, QVariant> mParameters;
+};
+
+/**
+ * @short Base class for various DAV groupware dialects.
+ *
+ * This class provides an interface to query the DAV dialect
+ * specific features and abstract them.
+ *
+ * The functionality is implemented in:
+ *   @li CaldavProtocol
+ *   @li CarddavProtocol
+ *   @li GroupdavProtocol
+ */
+class DavProtocolBase
+{
+public:
+    /**
+     * Destroys the dav protocol base.
+     */
+    virtual ~DavProtocolBase();
+
+    /**
+     * Returns whether the dav protocol dialect supports principal
+     * queries. If true, it must return the home set it provides
+     * access to with principalHomeSet() and the home set namespace
+     * with principalHomeSetNS();
+     */
+    virtual bool supportsPrincipals() const = 0;
+
+    /**
+     * Returns whether the dav protocol dialect supports the REPORT
+     * command to query all resources of a collection.
+     * If not, PROPFIND command will be used instead.
+     */
+    virtual bool useReport() const = 0;
+
+    /**
+     * Returns whether the dav protocol dialect supports the MULTIGET command.
+     *
+     * If MULTIGET is supported, the content of all dav resources
+     * can be fetched in ResourceBase::retrieveItems() already and
+     * there is no need to call ResourceBase::retrieveItem() for every single
+     * dav resource.
+     *
+     * Protocols that have MULTIGET capabilities must inherit from
+     * DavMultigetProtocol instead of this class.
+     */
+    virtual bool useMultiget() const = 0;
+
+    /**
+     * Returns the home set that this protocol supports.
+     */
+    virtual QString principalHomeSet() const;
+
+    /**
+     * Returns the namespace of the home set.
+     */
+    virtual QString principalHomeSetNS() const;
+
+    /**
+     * Returns the XML document that represents the DAV query to
+     * list all available DAV collections.
+     */
+    virtual XMLQueryBuilder::Ptr collectionsQuery() const = 0;
+
+    /**
+     * Returns the XQuery string that filters out the relevant XML elements
+     * from the result returned by the query that is provided by collectionQuery().
+     */
+    virtual QString collectionsXQuery() const = 0;
+
+    /**
+     * Returns a list of XML documents that represent DAV queries to
+     * list all available DAV resources inside a specific DAV collection.
+     */
+    virtual QVector<XMLQueryBuilder::Ptr> itemsQueries() const = 0;
+
+    /**
+     * Returns the possible content types for the collection that
+     * is described by the passed @p propstat element of a PROPFIND result.
+     */
+    virtual DavCollection::ContentTypes collectionContentTypes(const QDomElement &propstat) const = 0;
+};
+
+#endif
diff --git a/resources/dav/common/davutils.cpp b/resources/dav/common/davutils.cpp
new file mode 100644 (file)
index 0000000..ad42579
--- /dev/null
@@ -0,0 +1,379 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davutils.h"
+#include "davitem.h"
+#include "davmanager.h"
+#include "davprotocolattribute.h"
+#include "davprotocolbase.h"
+
+#include <collection.h>
+#include <item.h>
+#include <kcontacts/addressee.h>
+#include <kcontacts/vcardconverter.h>
+#include <KCalCore/ICalFormat>
+#include <KCalCore/Incidence>
+#include <KCalCore/MemoryCalendar>
+#include <KLocalizedString>
+
+#include <QtCore/QByteArray>
+#include <QtCore/QString>
+
+typedef QSharedPointer<KCalCore::Incidence> IncidencePtr;
+
+QDomElement DavUtils::firstChildElementNS(const QDomElement &parent, const QString &namespaceUri, const QString &tagName)
+{
+    for (QDomNode child = parent.firstChild(); !child.isNull(); child = child.nextSibling()) {
+        if (child.isElement()) {
+            const QDomElement elt = child.toElement();
+            if (tagName.isEmpty() || (elt.tagName() == tagName && elt.namespaceURI() == namespaceUri)) {
+                return elt;
+            }
+        }
+    }
+
+    return QDomElement();
+}
+
+QDomElement DavUtils::nextSiblingElementNS(const QDomElement &element, const QString &namespaceUri, const QString &tagName)
+{
+    for (QDomNode sib = element.nextSibling(); !sib.isNull(); sib = sib.nextSibling()) {
+        if (sib.isElement()) {
+            const QDomElement elt = sib.toElement();
+            if (tagName.isEmpty() || (elt.tagName() == tagName && elt.namespaceURI() == namespaceUri)) {
+                return elt;
+            }
+        }
+    }
+
+    return QDomElement();
+}
+
+DavUtils::Privileges DavUtils::extractPrivileges(const QDomElement &element)
+{
+    Privileges final = None;
+    QDomElement privElement = firstChildElementNS(element, QStringLiteral("DAV:"), QStringLiteral("privilege"));
+
+    while (!privElement.isNull()) {
+        QDomElement child = privElement.firstChildElement();
+
+        while (!child.isNull()) {
+            final |= parsePrivilege(child);
+            child = child.nextSiblingElement();
+        }
+
+        privElement = DavUtils::nextSiblingElementNS(privElement, QStringLiteral("DAV:"), QStringLiteral("privilege"));
+    }
+
+    return final;
+}
+
+DavUtils::Privileges DavUtils::parsePrivilege(const QDomElement &element)
+{
+    Privileges final = None;
+
+    if (!element.childNodes().isEmpty()) {
+        // This is an aggregate privilege, parse each of its children
+        QDomElement child = element.firstChildElement();
+        while (!child.isNull()) {
+            final |= parsePrivilege(child);
+            child = child.nextSiblingElement();
+        }
+    } else {
+        // This is a normal privilege
+        const QString privname = element.localName();
+
+        if (privname == QLatin1String("read")) {
+            final |= DavUtils::Read;
+        } else if (privname == QLatin1String("write")) {
+            final |= DavUtils::Write;
+        } else if (privname == QLatin1String("write-properties")) {
+            final |= DavUtils::WriteProperties;
+        } else if (privname == QLatin1String("write-content")) {
+            final |= DavUtils::WriteContent;
+        } else if (privname == QLatin1String("unlock")) {
+            final |= DavUtils::Unlock;
+        } else if (privname == QLatin1String("read-acl")) {
+            final |= DavUtils::ReadAcl;
+        } else if (privname == QLatin1String("read-current-user-privilege-set")) {
+            final |= DavUtils::ReadCurrentUserPrivilegeSet;
+        } else if (privname == QLatin1String("write-acl")) {
+            final |= DavUtils::WriteAcl;
+        } else if (privname == QLatin1String("bind")) {
+            final |= DavUtils::Bind;
+        } else if (privname == QLatin1String("unbind")) {
+            final |= DavUtils::Unbind;
+        } else if (privname == QLatin1String("all")) {
+            final |= DavUtils::All;
+        }
+    }
+
+    return final;
+}
+
+DavUtils::DavUrl::DavUrl()
+    : mProtocol(CalDav)
+{
+}
+
+DavUtils::DavUrl::DavUrl(const QUrl &url, DavUtils::Protocol protocol)
+    : mUrl(url), mProtocol(protocol)
+{
+}
+
+void DavUtils::DavUrl::setUrl(const QUrl &url)
+{
+    mUrl = url;
+}
+
+QUrl DavUtils::DavUrl::url() const
+{
+    return mUrl;
+}
+
+void DavUtils::DavUrl::setProtocol(DavUtils::Protocol protocol)
+{
+    mProtocol = protocol;
+}
+
+DavUtils::Protocol DavUtils::DavUrl::protocol() const
+{
+    return mProtocol;
+}
+
+QLatin1String DavUtils::protocolName(DavUtils::Protocol protocol)
+{
+    QLatin1String protocolName("");
+
+    switch (protocol) {
+    case DavUtils::CalDav:
+        protocolName = QLatin1String("CalDav");
+        break;
+    case DavUtils::CardDav:
+        protocolName = QLatin1String("CardDav");
+        break;
+    case DavUtils::GroupDav:
+        protocolName = QLatin1String("GroupDav");
+        break;
+    }
+
+    return protocolName;
+}
+
+QString DavUtils::translatedProtocolName(DavUtils::Protocol protocol)
+{
+    QString protocolName;
+
+    switch (protocol) {
+    case DavUtils::CalDav:
+        protocolName = i18n("CalDav");
+        break;
+    case DavUtils::CardDav:
+        protocolName = i18n("CardDav");
+        break;
+    case DavUtils::GroupDav:
+        protocolName = i18n("GroupDav");
+        break;
+    }
+
+    return protocolName;
+}
+
+DavUtils::Protocol DavUtils::protocolByName(const QString &name)
+{
+    DavUtils::Protocol protocol = DavUtils::CalDav;
+
+    if (name == QLatin1String("CalDav")) {
+        protocol = DavUtils::CalDav;
+    } else if (name == QLatin1String("CardDav")) {
+        protocol = DavUtils::CardDav;
+    } else if (name == QLatin1String("GroupDav")) {
+        protocol = DavUtils::GroupDav;
+    } else {
+        qCritical() << "Unexpected protocol name : " << name;
+    }
+
+    return protocol;
+}
+
+DavUtils::Protocol DavUtils::protocolByTranslatedName(const QString &name)
+{
+    DavUtils::Protocol protocol = DavUtils::CalDav;
+
+    if (name == i18n("CalDav")) {
+        protocol = DavUtils::CalDav;
+    } else if (name == i18n("CardDav")) {
+        protocol = DavUtils::CardDav;
+    } else if (name == i18n("GroupDav")) {
+        protocol = DavUtils::GroupDav;
+    }
+
+    return protocol;
+}
+
+QString DavUtils::createUniqueId()
+{
+    qint64 time = QDateTime::currentMSecsSinceEpoch() / 1000;
+    int r = qrand() % 1000;
+    QString id = QLatin1String("R") + QString::number(r);
+    QString uid = QString::number(time) + QLatin1String(".") + id;
+    return uid;
+}
+
+DavItem DavUtils::createDavItem(const Akonadi::Item &item, const Akonadi::Collection &collection, const Akonadi::Item::List &dependentItems)
+{
+    QByteArray rawData;
+    QString mimeType;
+    QUrl url;
+    DavItem davItem;
+    const QString basePath = collection.remoteId();
+
+    if (item.hasPayload<KContacts::Addressee>()) {
+        const KContacts::Addressee contact = item.payload<KContacts::Addressee>();
+        const QString fileName = createUniqueId();
+
+        url = QUrl(basePath + fileName + QLatin1String(".vcf"));
+
+        const DavProtocolAttribute *protoAttr = collection.attribute<DavProtocolAttribute>();
+        if (protoAttr) {
+            mimeType = DavUtils::contactsMimeType(DavUtils::Protocol(protoAttr->davProtocol()));
+        } else {
+            mimeType = KContacts::Addressee::mimeType();
+        }
+
+        KContacts::VCardConverter converter;
+        // rawData is already UTF-8
+        rawData = converter.exportVCard(contact, KContacts::VCardConverter::v3_0);
+    } else if (item.hasPayload<IncidencePtr>()) {
+        const KCalCore::MemoryCalendar::Ptr calendar(new KCalCore::MemoryCalendar(KDateTime::LocalZone));
+        calendar->addIncidence(item.payload<IncidencePtr>());
+        foreach (const Akonadi::Item &dependentItem, dependentItems) {
+            calendar->addIncidence(dependentItem.payload<IncidencePtr>());
+        }
+
+        const QString fileName = createUniqueId();
+
+        url = QUrl(basePath + fileName + QLatin1String(".ics"));
+        mimeType = QStringLiteral("text/calendar");
+
+        KCalCore::ICalFormat formatter;
+        rawData = formatter.toString(calendar, QString()).toUtf8();
+    }
+
+    davItem.setContentType(mimeType);
+    davItem.setData(rawData);
+    davItem.setUrl(url.toDisplayString());
+    davItem.setEtag(item.remoteRevision());
+
+    return davItem;
+}
+
+bool DavUtils::parseDavData(const DavItem &source, Akonadi::Item &target, Akonadi::Item::List &extraItems)
+{
+    const QString data = QString::fromUtf8(source.data());
+
+    if (target.mimeType() == KContacts::Addressee::mimeType()) {
+        KContacts::VCardConverter converter;
+        const KContacts::Addressee contact = converter.parseVCard(source.data());
+
+        if (contact.isEmpty()) {
+            return false;
+        }
+
+        target.setPayloadFromData(source.data());
+    } else {
+        KCalCore::ICalFormat formatter;
+        const KCalCore::MemoryCalendar::Ptr calendar(new KCalCore::MemoryCalendar(KDateTime::LocalZone));
+        formatter.fromString(calendar, data);
+        KCalCore::Incidence::List incidences = calendar->incidences();
+
+        if (incidences.isEmpty()) {
+            return false;
+        }
+
+        // All items must have the same uid in a single object.
+        // Find the main VEVENT (if that's indeed what we have,
+        // could be a VTODO or a VJOURNAL but that doesn't matter)
+        // and then apply the recurrence exceptions
+        IncidencePtr mainIncidence;
+        KCalCore::Incidence::List exceptions;
+
+        foreach (const IncidencePtr &incidence, incidences) {
+            if (incidence->hasRecurrenceId()) {
+                qDebug() << "Exception found with ID" << incidence->instanceIdentifier();
+                exceptions << incidence;
+            } else {
+                mainIncidence = incidence;
+            }
+        }
+
+        if (!mainIncidence) {
+            return false;
+        }
+
+        foreach (const IncidencePtr &exception, exceptions) {
+            if (exception->status() == KCalCore::Incidence::StatusCanceled) {
+                KDateTime exDateTime = exception->recurrenceId();
+                mainIncidence->recurrence()->addExDateTime(exDateTime);
+            } else {
+                // The exception remote id will contain a fragment pointing to
+                // its instance identifier to distinguish it from the main
+                // event.
+                QString rid = target.remoteId() + QLatin1String("#") + exception->instanceIdentifier();
+                qDebug() << "Extra incidence at" << rid;
+                Akonadi::Item extraItem = target;
+                extraItem.setRemoteId(rid);
+                extraItem.setRemoteRevision(source.etag());
+                extraItem.setMimeType(exception->mimeType());
+                extraItem.setPayload<IncidencePtr>(exception);
+                extraItems << extraItem;
+            }
+        }
+
+        target.setPayload<IncidencePtr>(mainIncidence);
+        // fix mime type for CalDAV collections
+        target.setMimeType(mainIncidence->mimeType());
+
+        /*
+        foreach ( const IncidencePtr &incidence, incidences ) {
+          QString rid = item.remoteId() + QLatin1String( "#" ) + incidence->instanceIdentifier();
+          Akonadi::Item extraItem = item;
+          extraItem.setRemoteId( rid );
+          extraItem.setRemoteRevision( davItem.etag() );
+          extraItem.setMimeType( incidence->mimeType() );
+          extraItem.setPayload<IncidencePtr>( incidence );
+          items << extraItem;
+        }
+        */
+    }
+
+    return true;
+}
+
+QString DavUtils::contactsMimeType(DavUtils::Protocol protocol)
+{
+    QString ret;
+
+    if (protocol == DavUtils::CardDav) {
+        ret = QStringLiteral("text/vcard");
+    } else if (protocol == DavUtils::GroupDav) {
+        ret = QStringLiteral("text/x-vcard");
+    }
+
+    return ret;
+}
diff --git a/resources/dav/common/davutils.h b/resources/dav/common/davutils.h
new file mode 100644 (file)
index 0000000..e8eeadd
--- /dev/null
@@ -0,0 +1,185 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVUTILS_H
+#define DAVUTILS_H
+
+#include <QtCore/QList>
+#include <QtCore/QUrl>
+#include <QtXml/QDomElement>
+
+#include <AkonadiCore/Item>
+
+class DavItem;
+namespace Akonadi
+{
+class Collection;
+class Item;
+}
+
+/**
+ * @short A namespace that contains helper methods for DAV functionality.
+ */
+namespace DavUtils
+{
+/**
+ * Describes the DAV protocol dialect.
+ */
+enum Protocol {
+    CalDav = 0,   ///< The CalDav protocol as defined in http://caldav.calconnect.org
+    CardDav,      ///< The CardDav protocol as defined in http://carddav.calconnect.org
+    GroupDav      ///< The GroupDav protocol as defined in http://www.groupdav.org
+};
+
+/**
+ * Describes the DAV privileges on a resource (see RFC3744)
+ */
+enum Privilege {
+    None = 0x0,
+    Read = 0x1,
+    Write = 0x2,
+    WriteProperties = 0x4,
+    WriteContent = 0x8,
+    Unlock = 0x10,
+    ReadAcl = 0x20,
+    ReadCurrentUserPrivilegeSet = 0x40,
+    WriteAcl = 0x80,
+    Bind = 0x100,
+    Unbind = 0x200,
+    All = 0x400
+};
+Q_DECLARE_FLAGS(Privileges, Privilege)
+Q_DECLARE_OPERATORS_FOR_FLAGS(Privileges)
+
+/**
+ * Returns the untranslated name of the given DAV @p protocol dialect.
+ */
+QLatin1String protocolName(Protocol protocol);
+
+/**
+ * Returns the i18n'ed name of the given DAV @p protocol dialect.
+ */
+QString translatedProtocolName(Protocol protocol);
+
+/**
+ * Returns the protocol matching the given name. This is the opposite of
+ * DavUtils::protocolName().
+ */
+Protocol protocolByName(const QString &name);
+
+/**
+ * Returns the protocol matching the given i18n'ed @p name. This is the opposite
+ * of DavUtils::translatedProtocolName().
+ */
+Protocol protocolByTranslatedName(const QString &name);
+
+/**
+ * @short A helper class to combine url and protocol of a DAV url.
+ */
+class DavUrl
+{
+public:
+    /**
+     * Defines a list of DAV url objects.
+     */
+    typedef QVector<DavUrl> List;
+
+    /**
+     * Creates an empty DAV url.
+     */
+    DavUrl();
+
+    /**
+     * Creates a new DAV url.
+     *
+     * @param url The url that identifies the DAV object.
+     * @param protocol The DAV protocol dialect that is used to retrieve the DAV object.
+     */
+    DavUrl(const QUrl &url, Protocol protocol);
+
+    /**
+     * Sets the @p url that identifies the DAV object.
+     */
+    void setUrl(const QUrl &url);
+
+    /**
+     * Returns the url that identifies the DAV object.
+     */
+    QUrl url() const;
+
+    /**
+     * Sets the DAV @p protocol dialect that is used to retrieve the DAV object.
+     */
+    void setProtocol(Protocol protocol);
+
+    /**
+     * Returns the DAV protocol dialect that is used to retrieve the DAV object.
+     */
+    Protocol protocol() const;
+
+private:
+    QUrl mUrl;
+    Protocol mProtocol;
+};
+
+/**
+ * Returns the first child element of @p parent that has the given @p tagName and is part of the @p namespaceUri.
+ */
+QDomElement firstChildElementNS(const QDomElement &parent, const QString &namespaceUri, const QString &tagName);
+
+/**
+ * Returns the next sibling element of @p element that has the given @p tagName and is part of the @p namespaceUri.
+ */
+QDomElement nextSiblingElementNS(const QDomElement &element, const QString &namespaceUri, const QString &tagName);
+
+/**
+ * Extracts privileges from @p element. The <privilege/> tags are expected to be first level children of @p element.
+ */
+Privileges extractPrivileges(const QDomElement &element);
+
+/**
+ * Parses a single <privilege/> tag and returns the final Privileges.
+ */
+Privileges parsePrivilege(const QDomElement &element);
+
+/**
+ * Creates a unique identifier that can be used as a file name to upload the dav item
+ */
+QString createUniqueId();
+
+/**
+ * Creates a new DavItem from the Akonadi::Item @p item.
+ *
+ * The returned item will have no payload (DavItem::data() will return an empty
+ * QByteArray) if the @p item payload is not recognized.
+ */
+DavItem createDavItem(const Akonadi::Item &item, const Akonadi::Collection &collection, const Akonadi::Item::List &dependentItems = Akonadi::Item::List());
+
+/**
+ * Parses the DAV data contained in @p source and puts it in @p target and @extraItems.
+ */
+bool parseDavData(const DavItem &source, Akonadi::Item &target, Akonadi::Item::List &extraItems);
+
+/**
+ * Returns the mimetype that shall be used for contact DAV resources using @p protocol.
+ */
+QString contactsMimeType(Protocol protocol);
+}
+
+Q_DECLARE_TYPEINFO(DavUtils::DavUrl, Q_MOVABLE_TYPE);
+#endif
diff --git a/resources/dav/common/etagcache.cpp b/resources/dav/common/etagcache.cpp
new file mode 100644 (file)
index 0000000..2344047
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+    Copyright (c) 2010 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "etagcache.h"
+
+#include <collection.h>
+#include <item.h>
+#include <itemfetchjob.h>
+#include <itemfetchscope.h>
+#include <kjob.h>
+
+EtagCache::EtagCache(const Akonadi::Collection &collection, QObject *parent)
+    : QObject(parent)
+{
+    Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob(collection);
+    job->fetchScope().fetchFullPayload(false);   // We only need the remote id and the revision
+    connect(job, &Akonadi::ItemFetchJob::result, this, &EtagCache::onItemFetchJobFinished);
+    job->start();
+}
+
+void EtagCache::setEtag(const QString &remoteId, const QString &etag)
+{
+    mCache[ remoteId ] = etag;
+
+    if (mChangedRemoteIds.contains(remoteId)) {
+        mChangedRemoteIds.remove(remoteId);
+    }
+}
+
+bool EtagCache::contains(const QString &remoteId)
+{
+    return mCache.contains(remoteId);
+}
+
+bool EtagCache::etagChanged(const QString &remoteId, const QString &refEtag) const
+{
+    return mCache.value(remoteId) != refEtag;
+}
+
+void EtagCache::markAsChanged(const QString &remoteId)
+{
+    mChangedRemoteIds.insert(remoteId);
+}
+
+bool EtagCache::isOutOfDate(const QString &remoteId) const
+{
+    return mChangedRemoteIds.contains(remoteId);
+}
+
+void EtagCache::removeEtag(const QString &remoteId)
+{
+    mChangedRemoteIds.remove(remoteId);
+    mCache.remove(remoteId);
+}
+
+QStringList EtagCache::urls() const
+{
+    return mCache.keys();
+}
+
+QStringList EtagCache::changedRemoteIds() const
+{
+    return mChangedRemoteIds.toList();
+}
+
+void EtagCache::onItemFetchJobFinished(KJob *job)
+{
+    if (job->error()) {
+        return;
+    }
+
+    const Akonadi::ItemFetchJob *fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job);
+    const Akonadi::Item::List items = fetchJob->items();
+
+    foreach (const Akonadi::Item &item, items) {
+        if (!mCache.contains(item.remoteId())) {
+            mCache[item.remoteId()] = item.remoteRevision();
+        }
+    }
+}
+
diff --git a/resources/dav/common/etagcache.h b/resources/dav/common/etagcache.h
new file mode 100644 (file)
index 0000000..d99c401
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+    Copyright (c) 2010 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef ETAGCACHE_H
+#define ETAGCACHE_H
+
+#include <QtCore/QMap>
+#include <QtCore/QObject>
+#include <QtCore/QSet>
+#include <QtCore/QStringList>
+
+namespace Akonadi
+{
+class Collection;
+}
+
+class KJob;
+
+/**
+ * @short A helper class to cache etags.
+ *
+ * The EtagCache caches the remote ids and etags of all items
+ * in a given collection. This cache is needed to find
+ * out which items have been changed in the backend and have to
+ * be refetched on the next call of ResourceBase::retrieveItems()
+ */
+class EtagCache : public QObject
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new etag cache and populates it with the ETags
+     * of items found in @p collection.
+     */
+    explicit EtagCache(const Akonadi::Collection &collection, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Sets the ETag for the remote ID. If the remote ID is marked as
+     * changed (is contained in the return of changedRemoteIds), remove
+     * it from the changed list.
+     */
+    void setEtag(const QString &remoteId, const QString &etag);
+
+    /**
+     * Checks if the given item is in the cache
+     */
+    bool contains(const QString &remoteId);
+
+    /**
+     * Check if the known ETag for the remote ID is equal to @p refEtag.
+     */
+    bool etagChanged(const QString &remoteId, const QString &refEtag) const;
+
+    /**
+     * Mark an item as changed in the backend.
+     */
+    void markAsChanged(const QString &remoteId);
+
+    /**
+     * Returns true if the remote ID is marked as changed (is contained in the
+     * return of changedRemoteIds)
+     */
+    bool isOutOfDate(const QString &remoteId) const;
+
+    /**
+     * Removes the entry for item with remote ID @p remoteId.
+     */
+    void removeEtag(const QString &remoteId);
+
+    /**
+     * Returns the list of all items URLs.
+     */
+    QStringList urls() const;
+
+    /**
+     * Returns the list of remote ids of items that have been changed
+     * in the backend.
+     */
+    QStringList changedRemoteIds() const;
+
+private Q_SLOTS:
+    void onItemFetchJobFinished(KJob *job);
+
+private:
+    QMap<QString, QString> mCache;
+    QSet<QString> mChangedRemoteIds;
+};
+
+#endif
diff --git a/resources/dav/protocols/caldavprotocol.cpp b/resources/dav/protocols/caldavprotocol.cpp
new file mode 100644 (file)
index 0000000..d84bc2b
--- /dev/null
@@ -0,0 +1,430 @@
+/*
+    Copyright (c) 2009 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "caldavprotocol.h"
+#include "davutils.h"
+
+#include <KCalCore/Event>
+#include <KCalCore/Journal>
+#include <KCalCore/Todo>
+
+#include <QtCore/QDateTime>
+#include <QtCore/QStringList>
+#include <QtCore/QUrl>
+#include <QtXml/QDomDocument>
+
+class CaldavCollectionQueryBuilder : public XMLQueryBuilder
+{
+public:
+    virtual QDomDocument buildQuery() const
+    {
+        QDomDocument document;
+
+        QDomElement propfindElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("propfind"));
+        document.appendChild(propfindElement);
+
+        QDomElement propElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
+        propfindElement.appendChild(propElement);
+
+        propElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("displayname")));
+        propElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("resourcetype")));
+        propElement.appendChild(document.createElementNS(QStringLiteral("http://apple.com/ns/ical/"), QStringLiteral("calendar-color")));
+        propElement.appendChild(document.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("supported-calendar-component-set")));
+        propElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("current-user-privilege-set")));
+        propElement.appendChild(document.createElementNS(QStringLiteral("http://calendarserver.org/ns/"), QStringLiteral("getctag")));
+
+        return document;
+    }
+
+    virtual QString mimeType() const
+    {
+        return QString();
+    }
+};
+
+class CaldavListEventQueryBuilder : public XMLQueryBuilder
+{
+public:
+    virtual QDomDocument buildQuery() const
+    {
+        QString startTime = parameter(QStringLiteral("start")).toString();
+        QString endTime = parameter(QStringLiteral("end")).toString();
+        QDomDocument document;
+
+        QDomElement queryElement = document.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("calendar-query"));
+        document.appendChild(queryElement);
+
+        QDomElement propElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
+        queryElement.appendChild(propElement);
+
+        QDomElement getetagElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("getetag"));
+        propElement.appendChild(getetagElement);
+
+        QDomElement getRTypeElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("resourcetype"));
+        propElement.appendChild(getRTypeElement);
+
+        QDomElement filterElement = document.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("filter"));
+        queryElement.appendChild(filterElement);
+
+        QDomElement compfilterElement = document.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("comp-filter"));
+
+        QDomAttr nameAttribute = document.createAttribute(QStringLiteral("name"));
+        nameAttribute.setValue(QStringLiteral("VCALENDAR"));
+        compfilterElement.setAttributeNode(nameAttribute);
+        filterElement.appendChild(compfilterElement);
+
+        QDomElement subcompfilterElement = document.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("comp-filter"));
+        nameAttribute = document.createAttribute(QStringLiteral("name"));
+        nameAttribute.setValue(QStringLiteral("VEVENT"));
+        subcompfilterElement.setAttributeNode(nameAttribute);
+
+        if (!startTime.isEmpty() || !endTime.isEmpty()) {
+            QDomElement timeRangeElement = document.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("time-range"));
+
+            if (!startTime.isEmpty()) {
+                QDomAttr startAttribute = document.createAttribute(QStringLiteral("start"));
+                startAttribute.setValue(startTime);
+                timeRangeElement.setAttributeNode(startAttribute);
+            }
+
+            if (!endTime.isEmpty()) {
+                QDomAttr endAttribute = document.createAttribute(QStringLiteral("end"));
+                endAttribute.setValue(endTime);
+                timeRangeElement.setAttributeNode(endAttribute);
+            }
+
+            subcompfilterElement.appendChild(timeRangeElement);
+        }
+
+        compfilterElement.appendChild(subcompfilterElement);
+
+        return document;
+    }
+
+    virtual QString mimeType() const
+    {
+        return KCalCore::Event::eventMimeType();
+    }
+};
+
+class CaldavListTodoQueryBuilder : public XMLQueryBuilder
+{
+public:
+    virtual QDomDocument buildQuery() const
+    {
+        QString startTime = parameter(QStringLiteral("start")).toString();
+        QString endTime = parameter(QStringLiteral("end")).toString();
+        QDomDocument document;
+
+        QDomElement queryElement = document.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("calendar-query"));
+        document.appendChild(queryElement);
+
+        QDomElement propElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
+        queryElement.appendChild(propElement);
+
+        QDomElement getetagElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("getetag"));
+        propElement.appendChild(getetagElement);
+
+        QDomElement getRTypeElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("resourcetype"));
+        propElement.appendChild(getRTypeElement);
+
+        QDomElement filterElement = document.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("filter"));
+        queryElement.appendChild(filterElement);
+
+        QDomElement compfilterElement = document.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("comp-filter"));
+
+        QDomAttr nameAttribute = document.createAttribute(QStringLiteral("name"));
+        nameAttribute.setValue(QStringLiteral("VCALENDAR"));
+        compfilterElement.setAttributeNode(nameAttribute);
+        filterElement.appendChild(compfilterElement);
+
+        QDomElement subcompfilterElement = document.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("comp-filter"));
+        nameAttribute = document.createAttribute(QStringLiteral("name"));
+        nameAttribute.setValue(QStringLiteral("VTODO"));
+        subcompfilterElement.setAttributeNode(nameAttribute);
+
+        if (!startTime.isEmpty() || !endTime.isEmpty()) {
+            QDomElement timeRangeElement = document.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("time-range"));
+
+            if (!startTime.isEmpty()) {
+                QDomAttr startAttribute = document.createAttribute(QStringLiteral("start"));
+                startAttribute.setValue(startTime);
+                timeRangeElement.setAttributeNode(startAttribute);
+            }
+
+            if (!endTime.isEmpty()) {
+                QDomAttr endAttribute = document.createAttribute(QStringLiteral("end"));
+                endAttribute.setValue(endTime);
+                timeRangeElement.setAttributeNode(endAttribute);
+            }
+
+            subcompfilterElement.appendChild(timeRangeElement);
+        }
+
+        compfilterElement.appendChild(subcompfilterElement);
+
+        return document;
+    }
+
+    virtual QString mimeType() const
+    {
+        return KCalCore::Todo::todoMimeType();
+    }
+};
+
+class CaldavListJournalQueryBuilder : public XMLQueryBuilder
+{
+public:
+    virtual QDomDocument buildQuery() const
+    {
+        QString startTime = parameter(QStringLiteral("start")).toString();
+        QString endTime = parameter(QStringLiteral("end")).toString();
+        QDomDocument document;
+
+        QDomElement queryElement = document.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("calendar-query"));
+        document.appendChild(queryElement);
+
+        QDomElement propElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
+        queryElement.appendChild(propElement);
+
+        QDomElement getetagElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("getetag"));
+        propElement.appendChild(getetagElement);
+
+        QDomElement getRTypeElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("resourcetype"));
+        propElement.appendChild(getRTypeElement);
+
+        QDomElement filterElement = document.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("filter"));
+        queryElement.appendChild(filterElement);
+
+        QDomElement compfilterElement = document.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("comp-filter"));
+
+        QDomAttr nameAttribute = document.createAttribute(QStringLiteral("name"));
+        nameAttribute.setValue(QStringLiteral("VCALENDAR"));
+        compfilterElement.setAttributeNode(nameAttribute);
+        filterElement.appendChild(compfilterElement);
+
+        QDomElement subcompfilterElement = document.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("comp-filter"));
+        nameAttribute = document.createAttribute(QStringLiteral("name"));
+        nameAttribute.setValue(QStringLiteral("VJOURNAL"));
+        subcompfilterElement.setAttributeNode(nameAttribute);
+
+        if (!startTime.isEmpty() || !endTime.isEmpty()) {
+            QDomElement timeRangeElement = document.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("time-range"));
+
+            if (!startTime.isEmpty()) {
+                QDomAttr startAttribute = document.createAttribute(QStringLiteral("start"));
+                startAttribute.setValue(startTime);
+                timeRangeElement.setAttributeNode(startAttribute);
+            }
+
+            if (!endTime.isEmpty()) {
+                QDomAttr endAttribute = document.createAttribute(QStringLiteral("end"));
+                endAttribute.setValue(endTime);
+                timeRangeElement.setAttributeNode(endAttribute);
+            }
+
+            subcompfilterElement.appendChild(timeRangeElement);
+        }
+
+        compfilterElement.appendChild(subcompfilterElement);
+
+        return document;
+    }
+
+    virtual QString mimeType() const
+    {
+        return KCalCore::Journal::journalMimeType();
+    }
+};
+
+class CaldavMultigetQueryBuilder : public XMLQueryBuilder
+{
+public:
+    virtual QDomDocument buildQuery() const
+    {
+        QDomDocument document;
+        QStringList urls = parameter(QStringLiteral("urls")).toStringList();
+        if (urls.isEmpty()) {
+            return document;
+        }
+
+        QDomElement multigetElement = document.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("calendar-multiget"));
+        document.appendChild(multigetElement);
+
+        QDomElement propElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
+        multigetElement.appendChild(propElement);
+
+        propElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("getetag")));
+        propElement.appendChild(document.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("calendar-data")));
+
+        foreach (const QString &url, urls) {
+            QDomElement hrefElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("href"));
+            const QUrl pathUrl = QUrl::fromUserInput(url);
+            QString encodedUrl = QString::fromAscii(pathUrl.encodedPath());
+            if (pathUrl.hasQuery()) {
+                encodedUrl.append(QStringLiteral("?"));
+                encodedUrl.append(QString::fromAscii(pathUrl.encodedQuery()));
+            }
+
+            const QDomText textNode = document.createTextNode(encodedUrl);
+            hrefElement.appendChild(textNode);
+
+            multigetElement.appendChild(hrefElement);
+        }
+
+        return document;
+    }
+
+    virtual QString mimeType() const
+    {
+        return QString();
+    }
+};
+
+CaldavProtocol::CaldavProtocol()
+{
+}
+
+bool CaldavProtocol::supportsPrincipals() const
+{
+    return true;
+}
+
+bool CaldavProtocol::useReport() const
+{
+    return true;
+}
+
+bool CaldavProtocol::useMultiget() const
+{
+    return true;
+}
+
+QString CaldavProtocol::principalHomeSet() const
+{
+    return QStringLiteral("calendar-home-set");
+}
+
+QString CaldavProtocol::principalHomeSetNS() const
+{
+    return QStringLiteral("urn:ietf:params:xml:ns:caldav");
+}
+
+XMLQueryBuilder::Ptr CaldavProtocol::collectionsQuery() const
+{
+    return XMLQueryBuilder::Ptr(new CaldavCollectionQueryBuilder());
+}
+
+QString CaldavProtocol::collectionsXQuery() const
+{
+    //const QString query( "//*[local-name()='calendar' and namespace-uri()='urn:ietf:params:xml:ns:caldav']/ancestor::*[local-name()='prop' and namespace-uri()='DAV:']/*[local-name()='supported-calendar-component-set' and namespace-uri()='urn:ietf:params:xml:ns:caldav']/*[local-name()='comp' and namespace-uri()='urn:ietf:params:xml:ns:caldav' and (@name='VTODO' or @name='VEVENT')]/ancestor::*[local-name()='response' and namespace-uri()='DAV:']" );
+    const QString query(QStringLiteral("//*[local-name()='calendar' and namespace-uri()='urn:ietf:params:xml:ns:caldav']/ancestor::*[local-name()='prop' and namespace-uri()='DAV:']/ancestor::*[local-name()='response' and namespace-uri()='DAV:']"));
+
+    return query;
+}
+
+QVector<XMLQueryBuilder::Ptr> CaldavProtocol::itemsQueries() const
+{
+    QVector<XMLQueryBuilder::Ptr> ret;
+    ret << XMLQueryBuilder::Ptr(new CaldavListEventQueryBuilder());
+    ret << XMLQueryBuilder::Ptr(new CaldavListTodoQueryBuilder());
+    ret << XMLQueryBuilder::Ptr(new CaldavListJournalQueryBuilder());
+    return ret;
+}
+
+XMLQueryBuilder::Ptr CaldavProtocol::itemsReportQuery(const QStringList &urls) const
+{
+    XMLQueryBuilder::Ptr ret(new CaldavMultigetQueryBuilder());
+    ret->setParameter(QStringLiteral("urls"), urls);
+    return ret;
+}
+
+QString CaldavProtocol::responseNamespace() const
+{
+    return QStringLiteral("urn:ietf:params:xml:ns:caldav");
+}
+
+QString CaldavProtocol::dataTagName() const
+{
+    return QStringLiteral("calendar-data");
+}
+
+DavCollection::ContentTypes CaldavProtocol::collectionContentTypes(const QDomElement &propstatElement) const
+{
+    /*
+     * Extract the content type information from a propstat like the following
+     *   <propstat xmlns="DAV:">
+     *     <prop xmlns="DAV:">
+     *       <C:supported-calendar-component-set xmlns:C="urn:ietf:params:xml:ns:caldav">
+     *         <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VEVENT"/>
+     *         <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VTODO"/>
+     *         <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VJOURNAL"/>
+     *         <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VTIMEZONE"/>
+     *         <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VFREEBUSY"/>
+     *       </C:supported-calendar-component-set>
+     *       <resourcetype xmlns="DAV:">
+     *         <collection xmlns="DAV:"/>
+     *         <C:calendar xmlns:C="urn:ietf:params:xml:ns:caldav"/>
+     *         <C:schedule-calendar xmlns:C="urn:ietf:params:xml:ns:caldav"/>
+     *       </resourcetype>
+     *       <displayname xmlns="DAV:">Test1 User</displayname>
+     *     </prop>
+     *     <status xmlns="DAV:">HTTP/1.1 200 OK</status>
+     *   </propstat>
+     */
+
+    const QDomElement propElement = DavUtils::firstChildElementNS(propstatElement, QStringLiteral("DAV:"), QStringLiteral("prop"));
+    const QDomElement supportedcomponentElement = DavUtils::firstChildElementNS(propElement, QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("supported-calendar-component-set"));
+
+    DavCollection::ContentTypes contentTypes;
+    QDomElement compElement = DavUtils::firstChildElementNS(supportedcomponentElement, QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("comp"));
+
+    /*
+     * Assign the content-type if the server didn't return anything.
+     * According to RFC4791, §5.2.3:
+     * In the absence of this property, the server MUST accept all
+     * component types, and the client can assume that all component
+     * types are accepted.
+     */
+    if (compElement.isNull()) {
+        contentTypes |= DavCollection::Calendar;
+        contentTypes |= DavCollection::Events;
+        contentTypes |= DavCollection::Todos;
+        contentTypes |= DavCollection::FreeBusy;
+        contentTypes |= DavCollection::Journal;
+    }
+
+    while (!compElement.isNull()) {
+        const QString type = compElement.attribute(QStringLiteral("name")).toLower();
+        if (type == QLatin1String("vcalendar")) {
+            contentTypes |= DavCollection::Calendar;
+        } else if (type == QLatin1String("vevent")) {
+            contentTypes |= DavCollection::Events;
+        } else if (type == QLatin1String("vtodo")) {
+            contentTypes |= DavCollection::Todos;
+        } else if (type == QLatin1String("vfreebusy")) {
+            contentTypes |= DavCollection::FreeBusy;
+        } else if (type == QLatin1String("vjournal")) {
+            contentTypes |= DavCollection::Journal;
+        }
+
+        compElement = DavUtils::nextSiblingElementNS(compElement, QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("comp"));
+    }
+
+    return contentTypes;
+}
diff --git a/resources/dav/protocols/caldavprotocol.h b/resources/dav/protocols/caldavprotocol.h
new file mode 100644 (file)
index 0000000..9a9748b
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+    Copyright (c) 2009 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef CALDAVPROTOCOL_H
+#define CALDAVPROTOCOL_H
+
+#include "davmultigetprotocol.h"
+
+class CaldavProtocol : public DavMultigetProtocol
+{
+public:
+    CaldavProtocol();
+    bool supportsPrincipals() const Q_DECL_OVERRIDE;
+    bool useReport() const Q_DECL_OVERRIDE;
+    bool useMultiget() const Q_DECL_OVERRIDE;
+    QString principalHomeSet() const Q_DECL_OVERRIDE;
+    QString principalHomeSetNS() const Q_DECL_OVERRIDE;
+    XMLQueryBuilder::Ptr collectionsQuery() const Q_DECL_OVERRIDE;
+    QString collectionsXQuery() const Q_DECL_OVERRIDE;
+    QVector<XMLQueryBuilder::Ptr> itemsQueries() const Q_DECL_OVERRIDE;
+    XMLQueryBuilder::Ptr itemsReportQuery(const QStringList &urls) const Q_DECL_OVERRIDE;
+    QString responseNamespace() const Q_DECL_OVERRIDE;
+    QString dataTagName() const Q_DECL_OVERRIDE;
+
+    DavCollection::ContentTypes collectionContentTypes(const QDomElement &propstat) const Q_DECL_OVERRIDE;
+};
+
+#endif
diff --git a/resources/dav/protocols/carddavprotocol.cpp b/resources/dav/protocols/carddavprotocol.cpp
new file mode 100644 (file)
index 0000000..9bba36c
--- /dev/null
@@ -0,0 +1,192 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "carddavprotocol.h"
+
+#include <kcontacts/addressee.h>
+
+#include <QtCore/QStringList>
+#include <QtXml/QDomDocument>
+
+class CarddavCollectionQueryBuilder : public XMLQueryBuilder
+{
+public:
+    virtual QDomDocument buildQuery() const
+    {
+        QDomDocument document;
+
+        QDomElement propfindElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("propfind"));
+        document.appendChild(propfindElement);
+
+        QDomElement propElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
+        propfindElement.appendChild(propElement);
+
+        propElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("displayname")));
+        propElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("resourcetype")));
+        propElement.appendChild(document.createElementNS(QStringLiteral("http://calendarserver.org/ns/"), QStringLiteral("getctag")));
+
+        return document;
+    }
+
+    virtual QString mimeType() const
+    {
+        return QString();
+    }
+};
+
+class CarddavListItemsQueryBuilder : public XMLQueryBuilder
+{
+public:
+    virtual QDomDocument buildQuery() const
+    {
+        QDomDocument document;
+
+        QDomElement propfindElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("propfind"));
+        document.appendChild(propfindElement);
+
+        QDomElement propElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
+        propfindElement.appendChild(propElement);
+
+        propElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("displayname")));
+        propElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("resourcetype")));
+        propElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("getetag")));
+
+        return document;
+    }
+
+    virtual QString mimeType() const
+    {
+        return KContacts::Addressee::mimeType();
+    }
+};
+
+class CarddavMultigetQueryBuilder : public XMLQueryBuilder
+{
+public:
+    virtual QDomDocument buildQuery() const
+    {
+        QDomDocument document;
+        QStringList urls = parameter(QStringLiteral("urls")).toStringList();
+        if (urls.isEmpty()) {
+            return document;
+        }
+
+        QDomElement multigetElement = document.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:carddav"), QStringLiteral("addressbook-multiget"));
+        document.appendChild(multigetElement);
+
+        QDomElement propElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
+        multigetElement.appendChild(propElement);
+
+        propElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("getetag")));
+        QDomElement addressDataElement = document.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:carddav"), QStringLiteral("address-data"));
+        addressDataElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("allprop")));
+        propElement.appendChild(addressDataElement);
+
+        foreach (const QString &url, urls) {
+            QDomElement hrefElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("href"));
+            const QUrl pathUrl = QUrl::fromUserInput(url);
+            QString encodedUrl = QString::fromAscii(pathUrl.encodedPath());
+            if (pathUrl.hasQuery()) {
+                encodedUrl.append(QStringLiteral("?"));
+                encodedUrl.append(QString::fromAscii(pathUrl.encodedQuery()));
+            }
+
+            const QDomText textNode = document.createTextNode(encodedUrl);
+            hrefElement.appendChild(textNode);
+
+            multigetElement.appendChild(hrefElement);
+        }
+
+        return document;
+    }
+
+    virtual QString mimeType() const
+    {
+        return QString();
+    }
+};
+
+CarddavProtocol::CarddavProtocol()
+{
+}
+
+bool CarddavProtocol::supportsPrincipals() const
+{
+    return true;
+}
+
+bool CarddavProtocol::useReport() const
+{
+    return false;
+}
+
+bool CarddavProtocol::useMultiget() const
+{
+    return true;
+}
+
+QString CarddavProtocol::principalHomeSet() const
+{
+    return QStringLiteral("addressbook-home-set");
+}
+
+QString CarddavProtocol::principalHomeSetNS() const
+{
+    return QStringLiteral("urn:ietf:params:xml:ns:carddav");
+}
+
+XMLQueryBuilder::Ptr CarddavProtocol::collectionsQuery() const
+{
+    return XMLQueryBuilder::Ptr(new CarddavCollectionQueryBuilder());
+}
+
+QString CarddavProtocol::collectionsXQuery() const
+{
+    const QString query(QStringLiteral("//*[local-name()='addressbook' and namespace-uri()='urn:ietf:params:xml:ns:carddav']/ancestor::*[local-name()='response' and namespace-uri()='DAV:']"));
+
+    return query;
+}
+
+QVector<XMLQueryBuilder::Ptr> CarddavProtocol::itemsQueries() const
+{
+    QVector<XMLQueryBuilder::Ptr> ret;
+    ret << XMLQueryBuilder::Ptr(new CarddavListItemsQueryBuilder());
+    return ret;
+}
+
+XMLQueryBuilder::Ptr CarddavProtocol::itemsReportQuery(const QStringList &urls) const
+{
+    XMLQueryBuilder::Ptr ret(new CarddavMultigetQueryBuilder());
+    ret->setParameter(QStringLiteral("urls"), urls);
+    return ret;
+}
+
+QString CarddavProtocol::responseNamespace() const
+{
+    return QStringLiteral("urn:ietf:params:xml:ns:carddav");
+}
+
+QString CarddavProtocol::dataTagName() const
+{
+    return QStringLiteral("address-data");
+}
+
+DavCollection::ContentTypes CarddavProtocol::collectionContentTypes(const QDomElement &) const
+{
+    return DavCollection::Contacts;
+}
diff --git a/resources/dav/protocols/carddavprotocol.h b/resources/dav/protocols/carddavprotocol.h
new file mode 100644 (file)
index 0000000..98176ed
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef CARDDAVPROTOCOL_H
+#define CARDDAVPROTOCOL_H
+
+#include "davmultigetprotocol.h"
+
+class CarddavProtocol : public DavMultigetProtocol
+{
+public:
+    CarddavProtocol();
+    bool supportsPrincipals() const Q_DECL_OVERRIDE;
+    bool useReport() const Q_DECL_OVERRIDE;
+    bool useMultiget() const Q_DECL_OVERRIDE;
+    QString principalHomeSet() const Q_DECL_OVERRIDE;
+    QString principalHomeSetNS() const Q_DECL_OVERRIDE;
+    XMLQueryBuilder::Ptr collectionsQuery() const Q_DECL_OVERRIDE;
+    QString collectionsXQuery() const Q_DECL_OVERRIDE;
+    QVector<XMLQueryBuilder::Ptr> itemsQueries() const Q_DECL_OVERRIDE;
+    XMLQueryBuilder::Ptr itemsReportQuery(const QStringList &urls) const Q_DECL_OVERRIDE;
+    QString responseNamespace() const Q_DECL_OVERRIDE;
+    QString dataTagName() const Q_DECL_OVERRIDE;
+
+    DavCollection::ContentTypes collectionContentTypes(const QDomElement &propstat) const Q_DECL_OVERRIDE;
+};
+
+#endif
diff --git a/resources/dav/protocols/groupdavprotocol.cpp b/resources/dav/protocols/groupdavprotocol.cpp
new file mode 100644 (file)
index 0000000..64d66f1
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+    Copyright (c) 2009 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "groupdavprotocol.h"
+
+#include "davutils.h"
+
+#include <QtXml/QDomDocument>
+
+class GroupdavCollectionQueryBuilder : public XMLQueryBuilder
+{
+public:
+    virtual QDomDocument buildQuery() const
+    {
+        QDomDocument document;
+
+        QDomElement propfindElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("propfind"));
+        document.appendChild(propfindElement);
+
+        QDomElement propElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
+        propfindElement.appendChild(propElement);
+
+        propElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("displayname")));
+        propElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("resourcetype")));
+
+        return document;
+    }
+
+    virtual QString mimeType() const
+    {
+        return QString();
+    }
+};
+
+class GroupdavItemQueryBuilder : public XMLQueryBuilder
+{
+public:
+    virtual QDomDocument buildQuery() const
+    {
+        QDomDocument document;
+
+        QDomElement propfindElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("propfind"));
+        document.appendChild(propfindElement);
+
+        QDomElement propElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
+        propfindElement.appendChild(propElement);
+
+        propElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("displayname")));
+        propElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("resourcetype")));
+        propElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("getetag")));
+
+        return document;
+    }
+
+    virtual QString mimeType() const
+    {
+        return QString();
+    }
+};
+
+GroupdavProtocol::GroupdavProtocol()
+{
+}
+
+bool GroupdavProtocol::supportsPrincipals() const
+{
+    return false;
+}
+
+bool GroupdavProtocol::useReport() const
+{
+    return false;
+}
+
+bool GroupdavProtocol::useMultiget() const
+{
+    return false;
+}
+
+XMLQueryBuilder::Ptr GroupdavProtocol::collectionsQuery() const
+{
+    return XMLQueryBuilder::Ptr(new GroupdavCollectionQueryBuilder());
+}
+
+QString GroupdavProtocol::collectionsXQuery() const
+{
+    const QString query(QStringLiteral("//*[(local-name()='vevent-collection' or local-name()='vtodo-collection' or local-name()='vcard-collection') and namespace-uri()='http://groupdav.org/']/ancestor::*[local-name()='response' and namespace-uri()='DAV:']"));
+
+    return query;
+}
+
+QVector<XMLQueryBuilder::Ptr> GroupdavProtocol::itemsQueries() const
+{
+    QVector<XMLQueryBuilder::Ptr> ret;
+    ret << XMLQueryBuilder::Ptr(new GroupdavItemQueryBuilder());
+    return ret;
+}
+
+DavCollection::ContentTypes GroupdavProtocol::collectionContentTypes(const QDomElement &propstatElement) const
+{
+    /*
+     * Extract the content type information from a propstat like the following
+     *
+     *  <propstat>
+     *    <status>HTTP/1.1 200 OK</status>
+     *    <prop>
+     *      <displayname>Tasks</displayname>
+     *      <resourcetype>
+     *        <collection/>
+     *        <G:vtodo-collection xmlns:G="http://groupdav.org/"/>
+     *      </resourcetype>
+     *      <getlastmodified>Sat, 30 Jan 2010 17:52:41 -0100</getlastmodified>
+     *    </prop>
+     *  </propstat>
+     */
+
+    const QDomElement propElement = DavUtils::firstChildElementNS(propstatElement, QStringLiteral("DAV:"), QStringLiteral("prop"));
+    const QDomElement resourcetypeElement = DavUtils::firstChildElementNS(propElement, QStringLiteral("DAV:"), QStringLiteral("resourcetype"));
+
+    DavCollection::ContentTypes contentTypes;
+
+    if (!DavUtils::firstChildElementNS(resourcetypeElement, QStringLiteral("http://groupdav.org/"), QStringLiteral("vevent-collection")).isNull()) {
+        contentTypes |= DavCollection::Events;
+    }
+
+    if (!DavUtils::firstChildElementNS(resourcetypeElement, QStringLiteral("http://groupdav.org/"), QStringLiteral("vtodo-collection")).isNull()) {
+        contentTypes |= DavCollection::Todos;
+    }
+
+    if (!DavUtils::firstChildElementNS(resourcetypeElement, QStringLiteral("http://groupdav.org/"), QStringLiteral("vcard-collection")).isNull()) {
+        contentTypes |= DavCollection::Contacts;
+    }
+
+    return contentTypes;
+}
diff --git a/resources/dav/protocols/groupdavprotocol.h b/resources/dav/protocols/groupdavprotocol.h
new file mode 100644 (file)
index 0000000..a71ffc9
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+    Copyright (c) 2009 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef GROUPDAVPROTOCOL_H
+#define GROUPDAVPROTOCOL_H
+
+#include "davprotocolbase.h"
+
+class GroupdavProtocol : public DavProtocolBase
+{
+public:
+    GroupdavProtocol();
+    bool supportsPrincipals() const Q_DECL_OVERRIDE;
+    bool useReport() const Q_DECL_OVERRIDE;
+    bool useMultiget() const Q_DECL_OVERRIDE;
+    XMLQueryBuilder::Ptr collectionsQuery() const Q_DECL_OVERRIDE;
+    QString collectionsXQuery() const Q_DECL_OVERRIDE;
+    QVector<XMLQueryBuilder::Ptr> itemsQueries() const Q_DECL_OVERRIDE;
+
+    DavCollection::ContentTypes collectionContentTypes(const QDomElement &propstat) const Q_DECL_OVERRIDE;
+};
+
+#endif
diff --git a/resources/dav/resource/CMakeLists.txt b/resources/dav/resource/CMakeLists.txt
new file mode 100644 (file)
index 0000000..3b2600d
--- /dev/null
@@ -0,0 +1,104 @@
+project(davgroupware)
+
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_davgroupware_resource\")
+
+#if (QT_QTXMLPATTERNS_LIBRARY)
+  if(WIN32)
+      set(LIB_INSTALL_DIR ${KDE_INSTALL_LIBDIR}
+                          RUNTIME DESTINATION ${KDE_INSTALL_BINDIR}
+                          LIBRARY DESTINATION ${KDE_INSTALL_LIBDIR}
+                          ARCHIVE DESTINATION ${KDE_INSTALL_LIBDIR} )
+  endif()
+
+  include_directories(
+      ../common/
+      ../protocols/
+  )
+
+  ########### next target ###############
+
+  set( davgroupwareresource_SRCS
+    ../common/davjobbase.cpp
+    ../common/davcollection.cpp
+    ../common/davcollectiondeletejob.cpp
+    ../common/davcollectionsfetchjob.cpp
+    ../common/davcollectionmodifyjob.cpp
+    ../common/davcollectionsmultifetchjob.cpp
+    ../common/davprotocolbase.cpp
+    ../common/davitem.cpp
+    ../common/davitemcreatejob.cpp
+    ../common/davitemdeletejob.cpp
+    ../common/davitemfetchjob.cpp
+    ../common/davitemmodifyjob.cpp
+    ../common/davitemsfetchjob.cpp
+    ../common/davitemslistjob.cpp
+    ../common/davmanager.cpp
+    ../common/davmultigetprotocol.cpp
+    ../common/davprincipalhomesetsfetchjob.cpp
+    ../common/davprincipalsearchjob.cpp
+    ../common/davutils.cpp
+    ../common/etagcache.cpp
+
+    ../protocols/caldavprotocol.cpp
+    ../protocols/carddavprotocol.cpp
+    ../protocols/groupdavprotocol.cpp
+    configdialog.cpp
+    ctagattribute.cpp
+    davfreebusyhandler.cpp
+    davgroupwareresource.cpp
+    davprotocolattribute.cpp
+    searchdialog.cpp
+    setupwizard.cpp
+    settings.cpp
+    urlconfigurationdialog.cpp
+  )
+
+ ecm_qt_declare_logging_category(davgroupwareresource_SRCS HEADER davresource_debug.h IDENTIFIER DAVRESOURCE_LOG CATEGORY_NAME log_davresource)
+
+  if(${AccountsQt_FOUND} AND ${SignOnQt_FOUND})
+    include_directories(${ACCOUNTSQT_INCLUDE_DIRS} ${SIGNONQT_INCLUDE_DIRS})
+    add_definitions(-DHAVE_ACCOUNTS)
+    set(davgroupwareresource_SRCS ../../shared/singlefileresource/getcredentialsjob.cpp ${davgroupwareresource_SRCS})
+  endif()
+
+  install( FILES davgroupwareresource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents" )
+  install( FILES davgroupwareprovider.desktop DESTINATION ${KDE_INSTALL_KSERVICETYPES5DIR} )
+
+  file( GLOB providersFiles "../services/*.desktop" )
+  install( FILES ${providersFiles} DESTINATION "${KDE_INSTALL_KSERVICES5DIR}/akonadi/davgroupware-providers" )
+
+  kconfig_add_kcfg_files(davgroupwareresource_SRCS settingsbase.kcfgc)
+  ki18n_wrap_ui(davgroupwareresource_SRCS configdialog.ui urlconfigurationdialog.ui searchdialog.ui)
+  kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/davgroupwareresource.kcfg org.kde.Akonadi.davGroupware.Settings)
+  qt5_add_dbus_adaptor(davgroupwareresource_SRCS
+    ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.davGroupware.Settings.xml settings.h Settings
+  )
+
+  add_executable(akonadi_davgroupware_resource ${davgroupwareresource_SRCS})
+
+  if( APPLE )
+    set_target_properties(akonadi_davgroupware_resource PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../../Info.plist.template)
+    set_target_properties(akonadi_davgroupware_resource PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.davGroupware")
+    set_target_properties(akonadi_davgroupware_resource PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi davGroupware Resource")
+  endif ()
+
+  target_link_libraries(akonadi_davgroupware_resource
+          Qt5::XmlPatterns
+          KF5::Contacts
+         KF5::AkonadiCalendar
+          KF5::KIOCore
+         KF5::AkonadiAgentBase
+          KF5::AkonadiCore
+          KF5::Wallet
+          KF5::CalendarCore)
+
+  if(${AccountsQt_FOUND} AND ${SignOnQt_FOUND})
+    target_link_libraries(akonadi_davgroupware_resource
+                        ${ACCOUNTSQT_LIBRARIES}
+                        ${SIGNONQT_LIBRARIES})
+  endif()
+  install(TARGETS akonadi_davgroupware_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
+#else()
+#  add_feature_info("Davgroupware resource" QT_QTXMLPATTERNS_LIBRARY "The QtXmlPatterns library was not found. It is needed for building the davgroupware resource.")
+#endif()
+
diff --git a/resources/dav/resource/Messages.sh b/resources/dav/resource/Messages.sh
new file mode 100644 (file)
index 0000000..dbbaca5
--- /dev/null
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+$EXTRACTRC *.ui *.kcfg >> rc.cpp
+$XGETTEXT *.cpp ../common/*.cpp -o $podir/akonadi_davgroupware_resource.pot
diff --git a/resources/dav/resource/akonadi-resources.png b/resources/dav/resource/akonadi-resources.png
new file mode 100644 (file)
index 0000000..8513893
Binary files /dev/null and b/resources/dav/resource/akonadi-resources.png differ
diff --git a/resources/dav/resource/configdialog.cpp b/resources/dav/resource/configdialog.cpp
new file mode 100644 (file)
index 0000000..917bba8
--- /dev/null
@@ -0,0 +1,330 @@
+/*
+    Copyright (c) 2009 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "configdialog.h"
+#include "searchdialog.h"
+#include "settings.h"
+#include "urlconfigurationdialog.h"
+
+#include <kconfigdialogmanager.h>
+#include <kconfigskeleton.h>
+#include <KLocalizedString>
+#include <kmessagebox.h>
+
+#include <QtCore/QList>
+#include <QtCore/QPointer>
+#include <QtCore/QStringList>
+#include <QStandardItem>
+#include <QStandardItemModel>
+#include <QVBoxLayout>
+#include <QDialogButtonBox>
+
+ConfigDialog::ConfigDialog(QWidget *parent)
+    : QDialog(parent)
+{
+    QWidget *mainWidget = new QWidget(this);
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    setLayout(mainLayout);
+    mainLayout->addWidget(mainWidget);
+    mUi.setupUi(mainWidget);
+    setWindowIcon(QIcon::fromTheme(QStringLiteral("folder-remote")));
+
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    mOkButton = buttonBox->button(QDialogButtonBox::Ok);
+    mOkButton->setDefault(true);
+    mOkButton->setShortcut(Qt::CTRL | Qt::Key_Return);
+    connect(buttonBox, &QDialogButtonBox::rejected, this, &ConfigDialog::onCancelClicked);
+    mainLayout->addWidget(buttonBox);
+
+    mModel = new QStandardItemModel();
+    QStringList headers;
+    headers << i18n("Protocol") << i18n("URL");
+    mModel->setHorizontalHeaderLabels(headers);
+
+    mUi.configuredUrls->setModel(mModel);
+    mUi.configuredUrls->setRootIsDecorated(false);
+
+    foreach (const DavUtils::DavUrl &url, Settings::self()->configuredDavUrls()) {
+        QUrl displayUrl = url.url();
+        displayUrl.setUserInfo(QString());
+        addModelRow(DavUtils::translatedProtocolName(url.protocol()), displayUrl.toDisplayString());
+    }
+
+    mUi.syncRangeStartType->addItem(i18n("Days"), QVariant(QLatin1String("D")));
+    mUi.syncRangeStartType->addItem(i18n("Months"), QVariant(QLatin1String("M")));
+    mUi.syncRangeStartType->addItem(i18n("Years"), QVariant(QLatin1String("Y")));
+
+    mManager = new KConfigDialogManager(this, Settings::self());
+    mManager->updateWidgets();
+
+    const int typeIndex = mUi.syncRangeStartType->findData(QVariant(Settings::self()->syncRangeStartType()));
+    mUi.syncRangeStartType->setCurrentIndex(typeIndex);
+
+    connect(mUi.kcfg_displayName, &KLineEdit::textChanged, this, &ConfigDialog::checkUserInput);
+    connect(mUi.configuredUrls->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ConfigDialog::checkConfiguredUrlsButtonsState);
+    connect(mUi.configuredUrls, &QAbstractItemView::doubleClicked, this, &ConfigDialog::onEditButtonClicked);
+
+    connect(mUi.syncRangeStartType, SIGNAL(currentIndexChanged(int)), this, SLOT(onSyncRangeStartTypeChanged()));
+    connect(mUi.addButton, &QPushButton::clicked, this, &ConfigDialog::onAddButtonClicked);
+    connect(mUi.searchButton, &QPushButton::clicked, this, &ConfigDialog::onSearchButtonClicked);
+    connect(mUi.removeButton, &QPushButton::clicked, this, &ConfigDialog::onRemoveButtonClicked);
+    connect(mUi.editButton, &QPushButton::clicked, this, &ConfigDialog::onEditButtonClicked);
+
+    connect(mOkButton, &QPushButton::clicked, this, &ConfigDialog::onOkClicked);
+
+    checkUserInput();
+    readConfig();
+}
+
+ConfigDialog::~ConfigDialog()
+{
+    writeConfig();
+}
+
+void ConfigDialog::readConfig()
+{
+    KConfigGroup grp(KSharedConfig::openConfig(), "ConfigDialog");
+    const QSize size = grp.readEntry("Size", QSize(300, 200));
+    if (size.isValid()) {
+        resize(size);
+    }
+}
+
+void ConfigDialog::writeConfig()
+{
+    KConfigGroup grp(KSharedConfig::openConfig(), "ConfigDialog");
+    grp.writeEntry("Size", size());
+    grp.sync();
+}
+
+void ConfigDialog::setPassword(const QString &password)
+{
+    mUi.password->setText(password);
+}
+
+void ConfigDialog::onSyncRangeStartTypeChanged()
+{
+    Settings::self()->setSyncRangeStartType(mUi.syncRangeStartType->currentData().toString());
+}
+
+void ConfigDialog::checkUserInput()
+{
+    checkConfiguredUrlsButtonsState();
+
+    if (!mUi.kcfg_displayName->text().trimmed().isEmpty() && !(mModel->invisibleRootItem()->rowCount() == 0)) {
+        mOkButton->setEnabled(true);
+    } else {
+        mOkButton->setEnabled(false);
+    }
+}
+
+void ConfigDialog::onAddButtonClicked()
+{
+    QPointer<UrlConfigurationDialog> dlg = new UrlConfigurationDialog(this);
+    dlg->setDefaultUsername(mUi.kcfg_defaultUsername->text());
+    dlg->setDefaultPassword(mUi.password->text());
+    const int result = dlg->exec();
+
+    if (result == QDialog::Accepted && !dlg.isNull()) {
+        if (Settings::self()->urlConfiguration(DavUtils::Protocol(dlg->protocol()), dlg->remoteUrl())) {
+            KMessageBox::error(this, i18n("Another configuration entry already uses the same URL/protocol couple.\n"
+                                          "Please use a different URL"));
+        } else {
+            Settings::UrlConfiguration *urlConfig = new Settings::UrlConfiguration();
+
+            urlConfig->mUrl = dlg->remoteUrl();
+            if (dlg->useDefaultCredentials()) {
+                urlConfig->mUser = QStringLiteral("$default$");
+            } else {
+                urlConfig->mUser = dlg->username();
+                urlConfig->mPassword = dlg->password();
+            }
+            urlConfig->mProtocol = dlg->protocol();
+
+            Settings::self()->newUrlConfiguration(urlConfig);
+
+            const QString protocolName = DavUtils::translatedProtocolName(dlg->protocol());
+
+            addModelRow(protocolName, dlg->remoteUrl());
+            mAddedUrls << QPair<QString, DavUtils::Protocol>(dlg->remoteUrl(), DavUtils::Protocol(dlg->protocol()));
+            checkUserInput();
+        }
+    }
+
+    delete dlg;
+}
+
+void ConfigDialog::onSearchButtonClicked()
+{
+    QPointer<SearchDialog> dlg = new SearchDialog(this);
+    dlg->setUsername(mUi.kcfg_defaultUsername->text());
+    dlg->setPassword(mUi.password->text());
+    const int result = dlg->exec();
+
+    if (result == QDialog::Accepted && !dlg.isNull()) {
+        const QStringList results = dlg->selection();
+        foreach (const QString &result, results) {
+            const QStringList split = result.split(QLatin1Char('|'));
+            DavUtils::Protocol protocol = DavUtils::protocolByName(split.at(0));
+            if (!Settings::self()->urlConfiguration(protocol, split.at(1))) {
+                Settings::UrlConfiguration *urlConfig = new Settings::UrlConfiguration();
+
+                urlConfig->mUrl = split.at(1);
+                if (dlg->useDefaultCredentials()) {
+                    urlConfig->mUser = QStringLiteral("$default$");
+                } else {
+                    urlConfig->mUser = dlg->username();
+                    urlConfig->mPassword = dlg->password();
+                }
+                urlConfig->mProtocol = protocol;
+
+                Settings::self()->newUrlConfiguration(urlConfig);
+
+                addModelRow(DavUtils::translatedProtocolName(protocol), split.at(1));
+                mAddedUrls << QPair<QString, DavUtils::Protocol>(split.at(1), protocol);
+                checkUserInput();
+            }
+        }
+    }
+
+    delete dlg;
+}
+
+void ConfigDialog::onRemoveButtonClicked()
+{
+    const QModelIndexList indexes = mUi.configuredUrls->selectionModel()->selectedRows();
+    if (indexes.size() == 0) {
+        return;
+    }
+
+    QString proto = mModel->index(indexes.at(0).row(), 0).data().toString();
+    QString url = mModel->index(indexes.at(0).row(), 1).data().toString();
+
+    mRemovedUrls << QPair<QString, DavUtils::Protocol>(url, DavUtils::protocolByTranslatedName(proto));
+    mModel->removeRow(indexes.at(0).row());
+
+    checkUserInput();
+}
+
+void ConfigDialog::onEditButtonClicked()
+{
+    const QModelIndexList indexes = mUi.configuredUrls->selectionModel()->selectedRows();
+    if (indexes.size() == 0) {
+        return;
+    }
+
+    const int row = indexes.at(0).row();
+    const QString proto = mModel->index(row, 0).data().toString();
+    const QString url = mModel->index(row, 1).data().toString();
+
+    Settings::UrlConfiguration *urlConfig = Settings::self()->urlConfiguration(DavUtils::protocolByTranslatedName(proto), url);
+    if (!urlConfig) {
+        return;
+    }
+
+    QPointer<UrlConfigurationDialog> dlg = new UrlConfigurationDialog(this);
+    dlg->setRemoteUrl(urlConfig->mUrl);
+    dlg->setProtocol(DavUtils::Protocol(urlConfig->mProtocol));
+
+    if (urlConfig->mUser == QLatin1String("$default$")) {
+        dlg->setUseDefaultCredentials(true);
+    } else {
+        dlg->setUseDefaultCredentials(false);
+        dlg->setUsername(urlConfig->mUser);
+        dlg->setPassword(urlConfig->mPassword);
+    }
+    dlg->setDefaultUsername(mUi.kcfg_defaultUsername->text());
+    dlg->setDefaultPassword(mUi.password->text());
+
+    const int result = dlg->exec();
+
+    if (result == QDialog::Accepted && !dlg.isNull()) {
+        Settings::self()->removeUrlConfiguration(DavUtils::protocolByTranslatedName(proto), url);
+        Settings::UrlConfiguration *urlConfigAccepted = new Settings::UrlConfiguration();
+        urlConfigAccepted->mUrl = dlg->remoteUrl();
+        if (dlg->useDefaultCredentials()) {
+            urlConfigAccepted->mUser = QStringLiteral("$default$");
+        } else {
+            urlConfigAccepted->mUser = dlg->username();
+            urlConfigAccepted->mPassword = dlg->password();
+        }
+        urlConfigAccepted->mProtocol = dlg->protocol();
+        Settings::self()->newUrlConfiguration(urlConfigAccepted);
+
+        mModel->removeRow(row);
+        insertModelRow(row, DavUtils::translatedProtocolName(dlg->protocol()), dlg->remoteUrl());
+    }
+    delete dlg;
+}
+
+void ConfigDialog::onOkClicked()
+{
+    typedef QPair<QString, DavUtils::Protocol> UrlPair;
+    foreach (const UrlPair &url, mRemovedUrls) {
+        Settings::self()->removeUrlConfiguration(url.second, url.first);
+    }
+
+    mManager->updateSettings();
+    Settings::self()->setDefaultPassword(mUi.password->text());
+    accept();
+}
+
+void ConfigDialog::onCancelClicked()
+{
+    mRemovedUrls.clear();
+
+    typedef QPair<QString, DavUtils::Protocol> UrlPair;
+    foreach (const UrlPair &url, mAddedUrls) {
+        Settings::self()->removeUrlConfiguration(url.second, url.first);
+    }
+    reject();
+}
+
+void ConfigDialog::checkConfiguredUrlsButtonsState()
+{
+    const bool enabled = mUi.configuredUrls->selectionModel()->hasSelection();
+
+    mUi.removeButton->setEnabled(enabled);
+    mUi.editButton->setEnabled(enabled);
+}
+
+void ConfigDialog::addModelRow(const QString &protocol, const QString &url)
+{
+    insertModelRow(-1, protocol, url);
+}
+
+void ConfigDialog::insertModelRow(int index, const QString &protocol, const QString &url)
+{
+    QStandardItem *rootItem = mModel->invisibleRootItem();
+    QList<QStandardItem *> items;
+
+    QStandardItem *protocolStandardItem = new QStandardItem(protocol);
+    protocolStandardItem->setEditable(false);
+    items << protocolStandardItem;
+
+    QStandardItem *urlStandardItem = new QStandardItem(url);
+    urlStandardItem->setEditable(false);
+    items << urlStandardItem;
+
+    if (index == -1) {
+        rootItem->appendRow(items);
+    } else {
+        rootItem->insertRow(index, items);
+    }
+}
+
diff --git a/resources/dav/resource/configdialog.h b/resources/dav/resource/configdialog.h
new file mode 100644 (file)
index 0000000..4f1ba21
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+    Copyright (c) 2009 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef CONFIGDIALOG_H
+#define CONFIGDIALOG_H
+
+#include "ui_configdialog.h"
+
+#include "davutils.h"
+
+#include <QDialog>
+
+#include <QtCore/QList>
+#include <QtCore/QPair>
+#include <QtCore/QString>
+class QPushButton;
+class KConfigDialogManager;
+class QStandardItemModel;
+
+class ConfigDialog : public QDialog
+{
+    Q_OBJECT
+
+public:
+    explicit ConfigDialog(QWidget *parent = Q_NULLPTR);
+    virtual ~ConfigDialog();
+
+    void setPassword(const QString &password);
+
+private Q_SLOTS:
+    void onSyncRangeStartTypeChanged();
+    void checkUserInput();
+    void onAddButtonClicked();
+    void onSearchButtonClicked();
+    void onRemoveButtonClicked();
+    void onEditButtonClicked();
+    void checkConfiguredUrlsButtonsState();
+    void onOkClicked();
+    void onCancelClicked();
+
+private:
+    void readConfig();
+    void writeConfig();
+    void addModelRow(const QString &protocol, const QString &url);
+    void insertModelRow(int index, const QString &protocol, const QString &url);
+
+    Ui::ConfigDialog mUi;
+    KConfigDialogManager *mManager;
+    QList< QPair<QString, DavUtils::Protocol> > mAddedUrls;
+    QList< QPair<QString, DavUtils::Protocol> > mRemovedUrls;
+    QStandardItemModel *mModel;
+    QPushButton *mOkButton;
+};
+
+#endif
diff --git a/resources/dav/resource/configdialog.ui b/resources/dav/resource/configdialog.ui
new file mode 100644 (file)
index 0000000..03ce6e0
--- /dev/null
@@ -0,0 +1,363 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ConfigDialog</class>
+ <widget class="QWidget" name="ConfigDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>534</width>
+    <height>547</height>
+   </rect>
+  </property>
+  <property name="locale">
+   <locale language="English" country="UnitedStates"/>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_4">
+   <item>
+    <widget class="QGroupBox" name="groupBox_3">
+     <property name="title">
+      <string>General Configuration</string>
+     </property>
+     <layout class="QHBoxLayout" name="horizontalLayout_5">
+      <item>
+       <layout class="QFormLayout" name="formLayout">
+        <item row="0" column="0">
+         <widget class="QLabel" name="label_6">
+          <property name="locale">
+           <locale language="English" country="UnitedStates"/>
+          </property>
+          <property name="text">
+           <string>Display name:</string>
+          </property>
+         </widget>
+        </item>
+        <item row="0" column="1">
+         <widget class="KLineEdit" name="kcfg_displayName"/>
+        </item>
+        <item row="1" column="0">
+         <widget class="QLabel" name="label_2">
+          <property name="locale">
+           <locale language="English" country="UnitedStates"/>
+          </property>
+          <property name="text">
+           <string>Refresh every:</string>
+          </property>
+         </widget>
+        </item>
+        <item row="1" column="1">
+         <layout class="QHBoxLayout" name="horizontalLayout">
+          <item>
+           <widget class="QSpinBox" name="kcfg_refreshInterval">
+            <property name="locale">
+             <locale language="English" country="UnitedStates"/>
+            </property>
+            <property name="specialValueText">
+             <string>Never</string>
+            </property>
+            <property name="value">
+             <number>5</number>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QLabel" name="label_3">
+            <property name="locale">
+             <locale language="English" country="UnitedStates"/>
+            </property>
+            <property name="text">
+             <string>minutes</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <spacer name="horizontalSpacer">
+            <property name="orientation">
+             <enum>Qt::Horizontal</enum>
+            </property>
+            <property name="sizeHint" stdset="0">
+             <size>
+              <width>40</width>
+              <height>20</height>
+             </size>
+            </property>
+           </spacer>
+          </item>
+         </layout>
+        </item>
+        <item row="2" column="0">
+         <widget class="QLabel" name="label">
+          <property name="locale">
+           <locale language="English" country="UnitedStates"/>
+          </property>
+          <property name="text">
+           <string>Username:</string>
+          </property>
+         </widget>
+        </item>
+        <item row="2" column="1">
+         <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="2,1">
+          <item>
+           <widget class="QLineEdit" name="kcfg_defaultUsername">
+            <property name="locale">
+             <locale language="English" country="UnitedStates"/>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <spacer name="horizontalSpacer_2">
+            <property name="orientation">
+             <enum>Qt::Horizontal</enum>
+            </property>
+            <property name="sizeHint" stdset="0">
+             <size>
+              <width>40</width>
+              <height>20</height>
+             </size>
+            </property>
+           </spacer>
+          </item>
+         </layout>
+        </item>
+        <item row="3" column="0">
+         <widget class="QLabel" name="label_4">
+          <property name="locale">
+           <locale language="English" country="UnitedStates"/>
+          </property>
+          <property name="text">
+           <string>Password:</string>
+          </property>
+         </widget>
+        </item>
+        <item row="3" column="1">
+         <layout class="QHBoxLayout" name="horizontalLayout_4" stretch="2,1">
+          <item>
+           <widget class="QLineEdit" name="password">
+            <property name="locale">
+             <locale language="English" country="UnitedStates"/>
+            </property>
+            <property name="echoMode">
+             <enum>QLineEdit::Password</enum>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <spacer name="horizontalSpacer_3">
+            <property name="orientation">
+             <enum>Qt::Horizontal</enum>
+            </property>
+            <property name="sizeHint" stdset="0">
+             <size>
+              <width>40</width>
+              <height>20</height>
+             </size>
+            </property>
+           </spacer>
+          </item>
+         </layout>
+        </item>
+       </layout>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="groupBox">
+     <property name="locale">
+      <locale language="English" country="UnitedStates"/>
+     </property>
+     <property name="title">
+      <string>Server Configuration</string>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout_3">
+      <item>
+       <layout class="QHBoxLayout" name="horizontalLayout_3">
+        <item>
+         <widget class="QTreeView" name="configuredUrls"/>
+        </item>
+        <item>
+         <layout class="QVBoxLayout" name="verticalLayout">
+          <item>
+           <widget class="QPushButton" name="addButton">
+            <property name="text">
+             <string>Add</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QPushButton" name="searchButton">
+            <property name="text">
+             <string>Search</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QPushButton" name="removeButton">
+            <property name="text">
+             <string>Remove</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QPushButton" name="editButton">
+            <property name="text">
+             <string>Edit</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <spacer name="verticalSpacer_2">
+            <property name="orientation">
+             <enum>Qt::Vertical</enum>
+            </property>
+            <property name="sizeHint" stdset="0">
+             <size>
+              <width>20</width>
+              <height>40</height>
+             </size>
+            </property>
+           </spacer>
+          </item>
+         </layout>
+        </item>
+       </layout>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="groupBox_2">
+     <property name="title">
+      <string>Synchronization</string>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout_2">
+      <item>
+       <widget class="QCheckBox" name="kcfg_limitSyncRange">
+        <property name="toolTip">
+         <string>Select this if your calendars contain a lot of events and the server cannot fulfill the requests</string>
+        </property>
+        <property name="locale">
+         <locale language="English" country="UnitedStates"/>
+        </property>
+        <property name="text">
+         <string>Limit CalDav retrieval time range</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <layout class="QHBoxLayout" name="horizontalLayout_6">
+        <item>
+         <widget class="QLabel" name="label_5">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="locale">
+           <locale language="English" country="UnitedStates"/>
+          </property>
+          <property name="text">
+           <string>Only sync events more recent than</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QSpinBox" name="kcfg_syncRangeStartNumber">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="locale">
+           <locale language="English" country="UnitedStates"/>
+          </property>
+          <property name="minimum">
+           <number>1</number>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QComboBox" name="syncRangeStartType">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="locale">
+           <locale language="English" country="UnitedStates"/>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <spacer name="horizontalSpacer_4">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+       </layout>
+      </item>
+     </layout>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KLineEdit</class>
+   <extends>QLineEdit</extends>
+   <header>klineedit.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>kcfg_limitSyncRange</sender>
+   <signal>toggled(bool)</signal>
+   <receiver>label_5</receiver>
+   <slot>setEnabled(bool)</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>30</x>
+     <y>485</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>193</x>
+     <y>512</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>kcfg_limitSyncRange</sender>
+   <signal>toggled(bool)</signal>
+   <receiver>kcfg_syncRangeStartNumber</receiver>
+   <slot>setEnabled(bool)</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>264</x>
+     <y>488</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>267</x>
+     <y>516</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>kcfg_limitSyncRange</sender>
+   <signal>toggled(bool)</signal>
+   <receiver>syncRangeStartType</receiver>
+   <slot>setEnabled(bool)</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>332</x>
+     <y>489</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>331</x>
+     <y>510</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
diff --git a/resources/dav/resource/ctagattribute.cpp b/resources/dav/resource/ctagattribute.cpp
new file mode 100644 (file)
index 0000000..3cf9cfd
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+    Copyright (c) 2015 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "ctagattribute.h"
+
+CTagAttribute::CTagAttribute(const QString &ctag)
+    : mCTag(ctag)
+{
+}
+
+void CTagAttribute::setCTag(const QString &ctag)
+{
+    mCTag = ctag;
+}
+
+QString CTagAttribute::CTag() const
+{
+    return mCTag;
+}
+
+Akonadi::Attribute *CTagAttribute::clone() const
+{
+    return new CTagAttribute(mCTag);
+}
+
+QByteArray CTagAttribute::type() const
+{
+    static const QByteArray sType("ctag");
+    return sType;
+}
+
+QByteArray CTagAttribute::serialized() const
+{
+    return mCTag.toUtf8();
+}
+
+void CTagAttribute::deserialize(const QByteArray &data)
+{
+    mCTag = QString::fromUtf8(data);
+}
+
diff --git a/resources/dav/resource/ctagattribute.h b/resources/dav/resource/ctagattribute.h
new file mode 100644 (file)
index 0000000..b50760c
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+    Copyright (c) 2015 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef CTAGATTRIBUTE_H
+#define CTAGATTRIBUTE_H
+
+#include <AkonadiCore/attribute.h>
+
+#include <QtCore/QString>
+
+class CTagAttribute : public Akonadi::Attribute
+{
+public:
+    explicit CTagAttribute(const QString &ctag = QString());
+
+    void setCTag(const QString &ctag);
+    QString CTag() const;
+
+    Akonadi::Attribute *clone() const Q_DECL_OVERRIDE;
+    QByteArray type() const Q_DECL_OVERRIDE;
+    QByteArray serialized() const Q_DECL_OVERRIDE;
+    void deserialize(const QByteArray &data) Q_DECL_OVERRIDE;
+
+private:
+    QString mCTag;
+};
+
+#endif
+
diff --git a/resources/dav/resource/davfreebusyhandler.cpp b/resources/dav/resource/davfreebusyhandler.cpp
new file mode 100644 (file)
index 0000000..2446d16
--- /dev/null
@@ -0,0 +1,228 @@
+/*
+    Copyright (c) 2011 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davfreebusyhandler.h"
+
+#include "davcollectionsfetchjob.h"
+#include "davmanager.h"
+#include "davprincipalsearchjob.h"
+#include "settings.h"
+
+#include <KCalCore/ICalFormat>
+#include <KDateTime>
+#include <KLocalizedString>
+#include <kio/davjob.h>
+#include <kio/job.h>
+#include "davresource_debug.h"
+
+DavFreeBusyHandler::DavFreeBusyHandler(QObject *parent)
+    : QObject(parent), mNextRequestId(0)
+{
+}
+
+void DavFreeBusyHandler::canHandleFreeBusy(const QString &email)
+{
+    DavUtils::DavUrl::List urls = Settings::self()->configuredDavUrls();
+    foreach (const DavUtils::DavUrl &url, urls) {
+        if (url.protocol() == DavUtils::CalDav) {
+            ++mRequestsTracker[email].handlingJobCount;
+            DavPrincipalSearchJob *job = new DavPrincipalSearchJob(url, DavPrincipalSearchJob::EmailAddress, email);
+            job->setProperty("email", QVariant::fromValue(email));
+            job->setProperty("url", QVariant::fromValue(url.url().toString()));
+            job->fetchProperty(QStringLiteral("schedule-inbox-URL"), QStringLiteral("urn:ietf:params:xml:ns:caldav"));
+            connect(job, &DavPrincipalSearchJob::result, this, &DavFreeBusyHandler::onPrincipalSearchJobFinished);
+            job->start();
+        }
+    }
+}
+
+void DavFreeBusyHandler::retrieveFreeBusy(const QString &email, const KDateTime &start, const KDateTime &end)
+{
+    if (!mPrincipalScheduleOutbox.contains(email)) {
+        Q_EMIT freeBusyRetrieved(email, QString(), false,
+                                 i18n("No schedule-outbox found for %1", email));
+        return;
+    }
+
+    KCalCore::FreeBusy::Ptr fb(new KCalCore::FreeBusy(start, end));
+    KCalCore::Attendee::Ptr att(new KCalCore::Attendee(QString(), email));
+    fb->addAttendee(att);
+
+    KCalCore::ICalFormat formatter;
+    QByteArray fbData = formatter.createScheduleMessage(fb, KCalCore::iTIPRequest).toUtf8();
+
+    foreach (const QString &outbox, mPrincipalScheduleOutbox[email]) {
+        ++mRequestsTracker[email].retrievalJobCount;
+        uint requestId = mNextRequestId++;
+
+        QUrl url(outbox);
+        KIO::StoredTransferJob *job = KIO::storedHttpPost(fbData, url);
+        job->addMetaData(QStringLiteral("content-type"), QStringLiteral("text/calendar"));
+        job->setProperty("email", QVariant::fromValue(email));
+        job->setProperty("request-id", QVariant::fromValue(requestId));
+        connect(job, &DavPrincipalSearchJob::result, this, &DavFreeBusyHandler::onRetrieveFreeBusyJobFinished);
+        job->start();
+    }
+}
+
+void DavFreeBusyHandler::onPrincipalSearchJobFinished(KJob *job)
+{
+    QString email = job->property("email").toString();
+    int handlingJobCount = --mRequestsTracker[email].handlingJobCount;
+
+    if (job->error()) {
+        if (handlingJobCount == 0 && !mRequestsTracker[email].handlingJobSuccessful) {
+            Q_EMIT handlesFreeBusy(email, false);
+        }
+        return;
+    }
+
+    DavPrincipalSearchJob *davJob = qobject_cast<DavPrincipalSearchJob *>(job);
+    QList<DavPrincipalSearchJob::Result> results = davJob->results();
+
+    if (results.isEmpty()) {
+        if (handlingJobCount == 0 && !mRequestsTracker[email].handlingJobSuccessful) {
+            Q_EMIT handlesFreeBusy(email, false);
+        }
+        return;
+    }
+
+    mRequestsTracker[email].handlingJobSuccessful = true;
+
+    foreach (const DavPrincipalSearchJob::Result &result, results) {
+        qCDebug(DAVRESOURCE_LOG) << result.value;
+        QUrl url(davJob->property("url").toString());
+        if (result.value.startsWith(QLatin1Char('/'))) {
+            // href is only a path, use request url to complete
+            url.setPath(result.value, QUrl::TolerantMode);
+        } else {
+            // href is a complete url
+            url = QUrl::fromUserInput(result.value);
+        }
+
+        if (!mPrincipalScheduleOutbox[email].contains(url.url())) {
+            mPrincipalScheduleOutbox[email] << url.url();
+        }
+    }
+
+    if (handlingJobCount == 0) {
+        Q_EMIT handlesFreeBusy(email, true);
+    }
+}
+
+void DavFreeBusyHandler::onRetrieveFreeBusyJobFinished(KJob *job)
+{
+    QString email = job->property("email").toString();
+    uint requestId = job->property("request-id").toUInt();
+    int retrievalJobCount = --mRequestsTracker[email].retrievalJobCount;
+
+    if (job->error()) {
+        if (retrievalJobCount == 0 && !mRequestsTracker[email].retrievalJobSuccessful) {
+            emit(freeBusyRetrieved(email, QString(), false, job->errorString()));
+        }
+        return;
+    }
+
+    /*
+     * Extract info from a document like the following:
+     * <?xml version="1.0" encoding="utf-8" ?>
+     * <C:schedule-response xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
+     *   <C:response>
+     *     <C:recipient>
+     *       <D:href>mailto:wilfredo@example.com<D:href>
+     *     </C:recipient>
+     *     <C:request-status>2.0;Success</C:request-status>
+     *     <C:calendar-data>BEGIN:VCALENDAR
+     * VERSION:2.0
+     * PRODID:-//Example Corp.//CalDAV Server//EN
+     * METHOD:REPLY
+     * BEGIN:VFREEBUSY
+     * UID:4FD3AD926350
+     * DTSTAMP:20090602T200733Z
+     * DTSTART:20090602T000000Z
+     * DTEND:20090604T000000Z
+     * ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com
+     * ATTENDEE;CN="Wilfredo Sanchez Vega":mailto:wilfredo@example.com
+     * FREEBUSY;FBTYPE=BUSY:20090602T110000Z/20090602T120000Z
+     * FREEBUSY;FBTYPE=BUSY:20090603T170000Z/20090603T180000Z
+     * END:VFREEBUSY
+     * END:VCALENDAR
+     *     </C:calendar-data>
+     *   </C:response>
+     *   <C:response>
+     *     <C:recipient>
+     *       <D:href>mailto:mike@example.org<D:href>
+     *     </C:recipient>
+     *     <C:request-status>3.7;Invalid calendar user</C:request-status>
+     *   </C:response>
+     * </C:schedule-response>
+     */
+
+    KIO::StoredTransferJob *postJob = qobject_cast<KIO::StoredTransferJob *>(job);
+    QDomDocument response;
+    response.setContent(postJob->data(), true);
+
+    QDomElement scheduleResponse = response.documentElement();
+
+    // We are only expecting one response tag
+    QDomElement responseElement = DavUtils::firstChildElementNS(scheduleResponse, QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("response"));
+    if (responseElement.isNull()) {
+        if (retrievalJobCount == 0 && !mRequestsTracker[email].retrievalJobSuccessful) {
+            emit(freeBusyRetrieved(email, QString(), false, i18n("Invalid response from the server")));
+        }
+        return;
+    }
+
+    // We can load directly the calendar-data and use its content to create
+    // an incidence base that will give us everything we need to test
+    // the success
+    QDomElement calendarDataElement = DavUtils::firstChildElementNS(responseElement, QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("calendar-data"));
+    if (calendarDataElement.isNull()) {
+        if (retrievalJobCount == 0 && !mRequestsTracker[email].retrievalJobSuccessful) {
+            emit(freeBusyRetrieved(email, QString(), false, i18n("Invalid response from the server")));
+        }
+        return;
+    }
+
+    QString rawData = calendarDataElement.text();
+
+    KCalCore::ICalFormat format;
+    KCalCore::FreeBusy::Ptr fb = format.parseFreeBusy(rawData);
+    if (fb.isNull()) {
+        if (retrievalJobCount == 0 && !mRequestsTracker[email].retrievalJobSuccessful) {
+            emit(freeBusyRetrieved(email, QString(), false, i18n("Unable to parse free-busy data received")));
+        }
+        return;
+    }
+
+    // We're safe now
+    mRequestsTracker[email].retrievalJobSuccessful = true;
+
+//   fb->clearAttendees();
+    if (mRequestsTracker[email].resultingFreeBusy[requestId].isNull()) {
+        mRequestsTracker[email].resultingFreeBusy[requestId] = fb;
+    } else {
+        mRequestsTracker[email].resultingFreeBusy[requestId]->merge(fb);
+    }
+
+    if (retrievalJobCount == 0) {
+        QString fbStr = format.createScheduleMessage(mRequestsTracker[email].resultingFreeBusy[requestId],
+                        KCalCore::iTIPRequest);
+        Q_EMIT freeBusyRetrieved(email, fbStr, true, QString());
+    }
+}
diff --git a/resources/dav/resource/davfreebusyhandler.h b/resources/dav/resource/davfreebusyhandler.h
new file mode 100644 (file)
index 0000000..c839c39
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+    Copyright (c) 2011 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVFREEBUSYHANDLER_H
+#define DAVFREEBUSYHANDLER_H
+
+#include <KCalCore/FreeBusy>
+
+#include <QtCore/QMap>
+#include <QtCore/QObject>
+#include <QtCore/QString>
+
+class KDateTime;
+class KJob;
+
+/**
+ * @short The class that will manage DAV free-busy requests
+ */
+class DavFreeBusyHandler : public QObject
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Constructs a new DavFreeBusyHandler
+     */
+    explicit DavFreeBusyHandler(QObject *parent = Q_NULLPTR);
+
+    /**
+     * Checks if the free-busy info for @p email can be handled
+     *
+     * @param email The email address of the contact.
+     */
+    void canHandleFreeBusy(const QString &email);
+
+    /**
+     * Retrieve the free-busy info for @p email between @p start and @p end
+     *
+     * @param email The email address to retrieve the free-busy for
+     * @param start The start of the free-busy period to report
+     * @param end The end of the free-busy period to report
+     */
+    void retrieveFreeBusy(const QString &email, const KDateTime &start, const KDateTime &end);
+
+Q_SIGNALS:
+    /**
+     * Emitted once we know if the free-busy info for @p email
+     * can be handled or not.
+     */
+    void handlesFreeBusy(const QString &email, bool handles);
+
+    /**
+     * Emitted once the free-busy has been retrieved
+     */
+    void freeBusyRetrieved(const QString &email, const QString &freeBusy, bool success, const QString &errorText);
+
+private Q_SLOTS:
+    void onPrincipalSearchJobFinished(KJob *job);
+    void onRetrieveFreeBusyJobFinished(KJob *job);
+
+private:
+    /**
+     * Simple struct to track the state of requests
+     */
+    struct RequestTracker {
+        RequestTracker()
+            : handlingJobCount(0), handlingJobSuccessful(false),
+              retrievalJobCount(0), retrievalJobSuccessful(false)
+        {
+        }
+
+        int handlingJobCount;
+        bool handlingJobSuccessful;
+        int retrievalJobCount;
+        bool retrievalJobSuccessful;
+        QMap<uint, KCalCore::FreeBusy::Ptr> resultingFreeBusy;
+    };
+
+    QMap<QString, RequestTracker> mRequestsTracker;
+    QMap<QString, QStringList> mPrincipalScheduleOutbox;
+    uint mNextRequestId;
+};
+
+#endif // DAVFREEBUSYHANDLER_H
diff --git a/resources/dav/resource/davgroupwareprovider.desktop b/resources/dav/resource/davgroupwareprovider.desktop
new file mode 100644 (file)
index 0000000..16ee718
--- /dev/null
@@ -0,0 +1,72 @@
+[Desktop Entry]
+Type=ServiceType
+X-KDE-ServiceType=DavGroupwareProvider
+Name=DAV Groupware resource provider
+Name[bs]=DAV dobavljać grupnih resursa
+Name[ca]=Proveïdor de recurs de treball en grup DAV
+Name[ca@valencia]=Proveïdor de recurs de treball en grup DAV
+Name[da]=Ressourceudbyder til DAV-groupware
+Name[de]=DAV-Groupware-Ressource-Anbieter
+Name[el]=Πάροχος πόρου DAV groupware
+Name[en_GB]=DAV Groupware resource provider
+Name[es]=Proveedor de recurso de colaboración DAV
+Name[et]=DAV grupitöö ressursi pakkuja
+Name[fi]=DAV-työryhmäresurssitarjoaja
+Name[fr]=Fournisseur de ressources pour logiciels de collaboration DAV
+Name[ga]=Soláthraí acmhainní groupware DAV
+Name[gl]=Fornecedor de recursos de grupo DAV
+Name[hu]=DAV csoportmunka-erőforrás szolgáltató
+Name[ia]=Fornitor de ressource de DAV Groupware
+Name[it]=Fornitore di risorsa di groupware DAV
+Name[kk]=DAV топтық жұмыс ресурс провайдері
+Name[km]=ក្រុមហ៊ុន​ផ្ដល់​ធនធាន DAV Groupware
+Name[ko]=DAV 그룹웨어 자원 공급자
+Name[lt]=DAV grupinės įrangos resursų tiekėjas
+Name[lv]=DAV grupprogrammatūras resursa nodrošinātājs
+Name[nb]=DAV Groupware-ressursleverandør
+Name[nds]=Anbeder vun en DAV-Arbeitkoppel-Ressource
+Name[nl]=Leverancier van DAV-groupware-hulpbron
+Name[pl]=Dostawca zasobu Groupware DAV
+Name[pt]=Fornecedor de recursos de 'groupware' em DAV
+Name[pt_BR]=Provedor de recursos de groupware em DAV
+Name[ru]=Поставщик источников данных совместной работы DAV
+Name[sk]=Poskytovateľ zdroja DAV Groupware
+Name[sl]=Ponudnik vira za skupinsko delo DAV
+Name[sr]=Добављач групверских ДАВ ресурса
+Name[sr@ijekavian]=Добављач групверских ДАВ ресурса
+Name[sr@ijekavianlatin]=Dobavljač grupverskih DAV resursa
+Name[sr@latin]=Dobavljač grupverskih DAV resursa
+Name[sv]=Leverantör av DAV-grupprogramresurs
+Name[tr]=DAV Groupware kaynak sağlayıcı
+Name[uk]=Надавач ресурсу групової роботи DAV
+Name[x-test]=xxDAV Groupware resource providerxx
+Name[zh_CN]=DAV 群件资源提供方
+Name[zh_TW]=DAV 群組資源提供者
+
+[PropertyDef::X-DavGroupware-SupportedProtocols]
+Type=QStringList
+
+[PropertyDef::X-DavGroupware-InstallationPath]
+Type=QString
+
+[PropertyDef::X-DavGroupware-ProviderUsesSSL]
+Type=bool
+
+[PropertyDef::X-DavGroupware-CalDavHost]
+Type=QString
+
+[PropertyDef::X-DavGroupware-CalDavPath]
+Type=QString
+
+[PropertyDef::X-DavGroupware-CardDavHost]
+Type=QString
+
+[PropertyDef::X-DavGroupware-CardDavPath]
+Type=QString
+
+[PropertyDef::X-DavGroupware-GroupDavPath]
+Type=QString
+
+[PropertyDef::X-DavGroupware-Provider]
+Type=QString
+
diff --git a/resources/dav/resource/davgroupwareresource.cpp b/resources/dav/resource/davgroupwareresource.cpp
new file mode 100644 (file)
index 0000000..f427d86
--- /dev/null
@@ -0,0 +1,1306 @@
+/*
+    Copyright (c) 2009 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davgroupwareresource.h"
+
+#include "configdialog.h"
+#include "ctagattribute.h"
+#include "davcollectiondeletejob.h"
+#include "davcollectionsfetchjob.h"
+#include "davcollectionsmultifetchjob.h"
+#include "davfreebusyhandler.h"
+#include "davitemcreatejob.h"
+#include "davitemdeletejob.h"
+#include "davitemfetchjob.h"
+#include "davitemmodifyjob.h"
+#include "davitemsfetchjob.h"
+#include "davitemslistjob.h"
+#include "davmanager.h"
+#include "davprotocolattribute.h"
+#include "davprotocolbase.h"
+#include "settings.h"
+#include "settingsadaptor.h"
+#include "setupwizard.h"
+
+#include <KCalCore/FreeBusy>
+#include <KCalCore/Incidence>
+#include <KCalCore/ICalFormat>
+#include <KCalCore/MemoryCalendar>
+#include <KCalCore/Todo>
+#include <kdatetime.h>
+#include <kjob.h>
+
+#include <attributefactory.h>
+#include <cachepolicy.h>
+#include <changerecorder.h>
+#include <collectionfetchscope.h>
+#include <entitydisplayattribute.h>
+#include <itemfetchjob.h>
+#include <itemfetchscope.h>
+#include <AkonadiCore/recursiveitemfetchjob.h>
+#include <AkonadiCore/itemmodifyjob.h>
+#include <AkonadiCore/itemdeletejob.h>
+#include <AkonadiCore/collectionmodifyjob.h>
+#include <kcontacts/addressee.h>
+#include <kcontacts/vcardconverter.h>
+#include <kwindowsystem.h>
+#include <KLocalizedString>
+#include "davresource_debug.h"
+
+#include <QtCore/QSet>
+#include <QtDBus/QDBusConnection>
+
+using namespace Akonadi;
+
+typedef QSharedPointer<KCalCore::Incidence> IncidencePtr;
+
+DavGroupwareResource::DavGroupwareResource(const QString &id)
+    : ResourceBase(id), FreeBusyProviderBase(), mSyncErrorNotified(false)
+{
+    AttributeFactory::registerAttribute<EntityDisplayAttribute>();
+    AttributeFactory::registerAttribute<DavProtocolAttribute>();
+    AttributeFactory::registerAttribute<CTagAttribute>();
+
+    setNeedsNetwork(true);
+
+    mDavCollectionRoot.setParentCollection(Collection::root());
+    mDavCollectionRoot.setName(identifier());
+    mDavCollectionRoot.setRemoteId(identifier());
+    mDavCollectionRoot.setContentMimeTypes(QStringList() << Collection::mimeType());
+    mDavCollectionRoot.setRights(Collection::CanCreateCollection | Collection::CanDeleteCollection | Collection::CanChangeCollection);
+
+    EntityDisplayAttribute *attribute = mDavCollectionRoot.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
+    attribute->setIconName(QStringLiteral("folder-remote"));
+
+    int refreshInterval = Settings::self()->refreshInterval();
+    if (refreshInterval == 0) {
+        refreshInterval = -1;
+    }
+
+    Akonadi::CachePolicy cachePolicy;
+    cachePolicy.setInheritFromParent(false);
+    cachePolicy.setSyncOnDemand(false);
+    cachePolicy.setCacheTimeout(-1);
+    cachePolicy.setIntervalCheckTime(refreshInterval);
+    cachePolicy.setLocalParts(QStringList() << QStringLiteral("ALL"));
+    mDavCollectionRoot.setCachePolicy(cachePolicy);
+
+    changeRecorder()->fetchCollection(true);
+    changeRecorder()->collectionFetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All);
+    changeRecorder()->itemFetchScope().fetchFullPayload(true);
+    changeRecorder()->itemFetchScope().setAncestorRetrieval(ItemFetchScope::All);
+
+    Settings::self()->setWinId(winIdForDialogs());
+    Settings::self()->setResourceIdentifier(identifier());
+
+    mFreeBusyHandler = new DavFreeBusyHandler(this);
+    connect(mFreeBusyHandler, &DavFreeBusyHandler::handlesFreeBusy, this, &DavGroupwareResource::onHandlesFreeBusy);
+    connect(mFreeBusyHandler, &DavFreeBusyHandler::freeBusyRetrieved, this, &DavGroupwareResource::onFreeBusyRetrieved);
+
+    connect(this, &DavGroupwareResource::reloadConfiguration, this, &DavGroupwareResource::onReloadConfig);
+
+    scheduleCustomTask(this, "retrieveCollections", QVariant(), ResourceBase::Prepend);
+    scheduleCustomTask(this, "createInitialCache", QVariant(), ResourceBase::Prepend);
+}
+
+DavGroupwareResource::~DavGroupwareResource()
+{
+    delete mFreeBusyHandler;
+}
+
+void DavGroupwareResource::collectionRemoved(const Akonadi::Collection &collection)
+{
+    qCDebug(DAVRESOURCE_LOG) << "Removing collection " << collection.remoteId();
+
+    if (!configurationIsValid()) {
+        return;
+    }
+
+    const DavUtils::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(collection.remoteId());
+
+    DavCollectionDeleteJob *job = new DavCollectionDeleteJob(davUrl);
+    job->setProperty("collection", QVariant::fromValue(collection));
+    connect(job, &DavCollectionDeleteJob::result, this, &DavGroupwareResource::onCollectionRemovedFinished);
+    job->start();
+}
+
+void DavGroupwareResource::cleanup()
+{
+    Settings::self()->cleanup();
+    Akonadi::AgentBase::cleanup();
+}
+
+KDateTime DavGroupwareResource::lastCacheUpdate() const
+{
+    return KDateTime::currentLocalDateTime();
+}
+
+void DavGroupwareResource::canHandleFreeBusy(const QString &email) const
+{
+    if (!isOnline()) {
+        handlesFreeBusy(email, false);
+    } else {
+        mFreeBusyHandler->canHandleFreeBusy(email);
+    }
+}
+
+void DavGroupwareResource::onHandlesFreeBusy(const QString &email, bool handles)
+{
+    handlesFreeBusy(email, handles);
+}
+
+void DavGroupwareResource::retrieveFreeBusy(const QString &email, const KDateTime &start, const KDateTime &end)
+{
+    if (!isOnline()) {
+        freeBusyRetrieved(email, QString(), false, i18n("Unable to retrieve free-busy info while offline"));
+    } else {
+        mFreeBusyHandler->retrieveFreeBusy(email, start, end);
+    }
+}
+
+void DavGroupwareResource::onFreeBusyRetrieved(const QString &email, const QString &freeBusy, bool success, const QString &errorText)
+{
+    freeBusyRetrieved(email, freeBusy, success, errorText);
+}
+
+void DavGroupwareResource::configure(WId windowId)
+{
+    Settings::self()->setWinId(windowId);
+
+    // On the initial configuration we start the setup wizard
+    if (Settings::self()->configuredDavUrls().isEmpty()) {
+        SetupWizard wizard;
+
+        if (windowId) {
+            KWindowSystem::setMainWindow(&wizard, windowId);
+        }
+
+        const int result = wizard.exec();
+        if (result == QDialog::Accepted) {
+            const SetupWizard::Url::List urls = wizard.urls();
+            foreach (const SetupWizard::Url &url, urls) {
+                Settings::UrlConfiguration *urlConfig = new Settings::UrlConfiguration();
+
+                urlConfig->mUrl = url.url;
+                urlConfig->mProtocol = url.protocol;
+                urlConfig->mUser = url.userName;
+                urlConfig->mPassword = wizard.field(QStringLiteral("credentialsPassword")).toString();
+
+                Settings::self()->newUrlConfiguration(urlConfig);
+            }
+
+            if (!urls.isEmpty()) {
+                Settings::self()->setDisplayName(wizard.displayName());
+            }
+
+            QString defaultUser = wizard.field(QStringLiteral("credentialsUserName")).toString();
+            if (!defaultUser.isEmpty()) {
+                Settings::self()->setDefaultUsername(defaultUser);
+                Settings::self()->setDefaultPassword(wizard.field(QStringLiteral("credentialsPassword")).toString());
+            }
+        }
+    }
+
+    // continue with the normal config dialog
+    ConfigDialog dialog;
+
+    if (windowId) {
+        KWindowSystem::setMainWindow(&dialog, windowId);
+    }
+
+    if (!Settings::self()->defaultUsername().isEmpty()) {
+        dialog.setPassword(Settings::self()->defaultPassword());
+    }
+
+    const int result = dialog.exec();
+
+    if (result == QDialog::Accepted) {
+        Settings::self()->setSettingsVersion(3);
+        Settings::self()->save();
+        synchronize();
+        Q_EMIT configurationDialogAccepted();
+    } else {
+        Q_EMIT configurationDialogRejected();
+    }
+}
+
+void DavGroupwareResource::retrieveCollections()
+{
+    qCDebug(DAVRESOURCE_LOG) << "Retrieving collections list";
+    mSyncErrorNotified = false;
+
+    if (!configurationIsValid()) {
+        return;
+    }
+
+    Q_EMIT status(Running, i18n("Fetching collections"));
+
+    DavCollectionsMultiFetchJob *job = new DavCollectionsMultiFetchJob(Settings::self()->configuredDavUrls());
+    connect(job, &DavCollectionDeleteJob::result, this, &DavGroupwareResource::onRetrieveCollectionsFinished);
+    connect(job, &DavCollectionsMultiFetchJob::collectionDiscovered, this, &DavGroupwareResource::onCollectionDiscovered);
+    job->start();
+}
+
+void DavGroupwareResource::retrieveItems(const Akonadi::Collection &collection)
+{
+    if (!collection.isValid()) {
+        itemsRetrievalDone();
+        return;
+    }
+
+    qCDebug(DAVRESOURCE_LOG) << "Retrieving items for collection " << collection.remoteId();
+
+    if (!configurationIsValid()) {
+        return;
+    }
+
+    // As the resource root collection contains mime types for items we must
+    // work around the fact that Akonadi will rightfully try to retrieve items
+    // from it. So just return an empty list
+    if (collection.remoteId() == identifier()) {
+        itemsRetrievalDone();
+        return;
+    }
+
+    if (!mEtagCaches.contains(collection.remoteId())) {
+        qCDebug(DAVRESOURCE_LOG) << "Asked to retrieve items for a collection we don't have in the cache";
+        itemsRetrievalDone();
+        return;
+    }
+
+    // Only continue if the collection has changed or if
+    // it's the first time we see it
+    CTagAttribute *CTagAttr = collection.attribute<CTagAttribute>();
+    if (CTagAttr && mCTagCache.contains(collection.remoteId()) && mCTagCache.value(collection.remoteId()) == CTagAttr->CTag()) {
+        qCDebug(DAVRESOURCE_LOG) << "CTag for collection" << collection.remoteId() << "didn't change: " << CTagAttr->CTag();
+        itemsRetrievalDone();
+        return;
+    }
+
+    const DavUtils::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(collection.remoteId());
+
+    if (!davUrl.url().isValid()) {
+        qCCritical(DAVRESOURCE_LOG) << "Can't find a configured URL, collection.remoteId() is " << collection.remoteId();
+        cancelTask(i18n("Asked to retrieve items for an unknown collection: %1", collection.remoteId()));
+        //Q_ASSERT_X( false, "DavGroupwareResource::retrieveItems", "Url is invalid" );
+        return;
+    }
+
+    DavItemsListJob *job = new DavItemsListJob(davUrl, mEtagCaches.value(collection.remoteId()));
+    if (Settings::self()->limitSyncRange()) {
+        QDateTime start = Settings::self()->getSyncRangeStart();
+        qCDebug(DAVRESOURCE_LOG) << "Start time for list job:" << start;
+        if (start.isValid()) {
+            job->setTimeRange(start.toString(QStringLiteral("yyyyMMddTHHMMssZ")), QString());
+        }
+    }
+    job->setProperty("collection", QVariant::fromValue(collection));
+    job->setContentMimeTypes(collection.contentMimeTypes());
+    connect(job, &DavCollectionDeleteJob::result, this, &DavGroupwareResource::onRetrieveItemsFinished);
+    job->start();
+}
+
+bool DavGroupwareResource::retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &)
+{
+    qCDebug(DAVRESOURCE_LOG) << "Retrieving single item. Remote id = " << item.remoteId();
+
+    if (!configurationIsValid()) {
+        return false;
+    }
+
+    const DavUtils::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(item.parentCollection().remoteId(), item.remoteId());
+    if (!davUrl.url().isValid()) {
+        qCDebug(DAVRESOURCE_LOG) << "Failed to get a valid DavUrl. Parent collection remote ID is" << item.parentCollection().remoteId();
+        cancelTask();
+        return false;
+    }
+
+    DavItem davItem;
+    davItem.setUrl(item.remoteId());
+    davItem.setContentType(QStringLiteral("text/calendar"));
+    davItem.setEtag(item.remoteRevision());
+
+    DavItemFetchJob *job = new DavItemFetchJob(davUrl, davItem);
+    job->setProperty("item", QVariant::fromValue(item));
+    connect(job, &DavCollectionDeleteJob::result, this, &DavGroupwareResource::onRetrieveItemFinished);
+    job->start();
+
+    return true;
+}
+
+void DavGroupwareResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection)
+{
+    qCDebug(DAVRESOURCE_LOG) << "Received notification for added item. Local id = "
+                             << item.id() << ". Remote id = " << item.remoteId()
+                             << ". Collection remote id = " << collection.remoteId();
+
+    if (!configurationIsValid()) {
+        return;
+    }
+
+    if (collection.remoteId().isEmpty()) {
+        qCCritical(DAVRESOURCE_LOG) << "Invalid remote id for collection " << collection.id() << " = " << collection.remoteId();
+        cancelTask(i18n("Invalid collection for item %1.", item.id()));
+        return;
+    }
+
+    DavItem davItem = DavUtils::createDavItem(item, collection);
+    if (davItem.data().isEmpty()) {
+        qCCritical(DAVRESOURCE_LOG) << "Item " << item.id() << " doesn't has a valid payload";
+        cancelTask();
+        return;
+    }
+
+    QString urlStr = davItem.url();
+    const DavUtils::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(collection.remoteId(), urlStr);
+    qCDebug(DAVRESOURCE_LOG) << "Item " << item.id() << " will be put to " << urlStr;
+
+    DavItemCreateJob *job = new DavItemCreateJob(davUrl, davItem);
+    job->setProperty("collection", QVariant::fromValue(collection));
+    job->setProperty("item", QVariant::fromValue(item));
+    connect(job, &DavCollectionDeleteJob::result, this, &DavGroupwareResource::onItemAddedFinished);
+    job->start();
+}
+
+void DavGroupwareResource::itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &)
+{
+    qCDebug(DAVRESOURCE_LOG) << "Received notification for changed item. Local id = " << item.id()
+                             << ". Remote id = " << item.remoteId();
+
+    if (!configurationIsValid()) {
+        return;
+    }
+
+    const Akonadi::Collection collection = item.parentCollection();
+    if (!mEtagCaches.contains(collection.remoteId())) {
+        qCDebug(DAVRESOURCE_LOG) << "Changed item is in a collection we don't have in the cache";
+        // TODO: display an error
+        cancelTask();
+        return;
+    }
+
+    QString ridBase = item.remoteId();
+    if (ridBase.contains(QLatin1Char('#'))) {
+        ridBase.truncate(ridBase.indexOf(QLatin1Char('#')));
+    }
+
+    EtagCache *cache = mEtagCaches.value(collection.remoteId());
+    Akonadi::Item::List extraItems;
+    foreach (const QString &rid, cache->urls()) {
+        if (rid.startsWith(ridBase) && rid != item.remoteId()) {
+            Akonadi::Item extraItem;
+            extraItem.setRemoteId(rid);
+            extraItems << extraItem;
+        }
+    }
+
+    if (extraItems.isEmpty()) {
+        doItemChange(item);
+    } else {
+        Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob(extraItems);
+        job->setCollection(item.parentCollection());
+        job->fetchScope().fetchFullPayload();
+        job->setProperty("item", QVariant::fromValue(item));
+        connect(job, &Akonadi::ItemFetchJob::result, this, &DavGroupwareResource::onItemChangePrepared);
+    }
+}
+
+void DavGroupwareResource::onItemChangePrepared(KJob *job)
+{
+    Akonadi::ItemFetchJob *fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job);
+    Akonadi::Item item = job->property("item").value<Akonadi::Item>();
+    doItemChange(item, fetchJob->items());
+}
+
+void DavGroupwareResource::doItemChange(const Akonadi::Item &item, const Akonadi::Item::List &dependentItems)
+{
+    DavItem davItem = DavUtils::createDavItem(item, item.parentCollection(), dependentItems);
+    if (davItem.data().isEmpty()) {
+        qCCritical(DAVRESOURCE_LOG) << "Item " << item.id() << " doesn't has a valid payload";
+        cancelTask();
+        return;
+    }
+
+    QString url = item.remoteId();
+    if (url.contains(QLatin1Char('#'))) {
+        url.truncate(url.indexOf(QLatin1Char('#')));
+    }
+    const DavUtils::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(item.parentCollection().remoteId(), url);
+
+    // We have to re-set the URL as it's not necessarily valid after createDavItem()
+    davItem.setUrl(url);
+    davItem.setEtag(item.remoteRevision());
+
+    DavItemModifyJob *modJob = new DavItemModifyJob(davUrl, davItem);
+    modJob->setProperty("collection", QVariant::fromValue(item.parentCollection()));
+    modJob->setProperty("item", QVariant::fromValue(item));
+    modJob->setProperty("dependentItems", QVariant::fromValue(dependentItems));
+    connect(modJob, &DavCollectionDeleteJob::result, this, &DavGroupwareResource::onItemChangedFinished);
+    modJob->start();
+}
+
+void DavGroupwareResource::itemRemoved(const Akonadi::Item &item)
+{
+    qCDebug(DAVRESOURCE_LOG) << "Received notification for removed item. Remote id = " << item.remoteId();
+
+    if (!configurationIsValid()) {
+        return;
+    }
+
+    const Akonadi::Collection collection = item.parentCollection();
+    if (!mEtagCaches.contains(collection.remoteId())) {
+        qCDebug(DAVRESOURCE_LOG) << "Removed item is in a collection we don't have in the cache";
+        // TODO: display an error
+        cancelTask();
+        return;
+    }
+
+    QString ridBase = item.remoteId();
+    if (ridBase.contains(QLatin1Char('#'))) {
+        // A bit tricky: we must remove an incidence contained in a resource
+        // containing multiple ones.
+        ridBase.truncate(ridBase.indexOf(QLatin1Char('#')));
+
+        EtagCache *cache = mEtagCaches.value(collection.remoteId());
+        Akonadi::Item::List extraItems;
+        foreach (const QString &rid, cache->urls()) {
+            if (rid.startsWith(ridBase) && rid != item.remoteId()) {
+                Akonadi::Item extraItem;
+                extraItem.setRemoteId(rid);
+                extraItems << extraItem;
+            }
+        }
+
+        if (extraItems.isEmpty()) {
+            // Urrrr?
+            // Well, just delete the item.
+            doItemRemoval(item);
+        } else {
+            Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob(extraItems);
+            job->setCollection(item.parentCollection());
+            job->fetchScope().fetchFullPayload();
+            job->setProperty("item", QVariant::fromValue(item));
+            connect(job, &Akonadi::ItemFetchJob::result, this, &DavGroupwareResource::onItemRemovalPrepared);
+        }
+    } else {
+        // easy as pie: just remove everything at the URL.
+        doItemRemoval(item);
+    }
+}
+
+void DavGroupwareResource::onItemRemovalPrepared(KJob *job)
+{
+    Akonadi::ItemFetchJob *fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job);
+    Akonadi::Item item = job->property("item").value<Akonadi::Item>();
+    Akonadi::Item::List keptItems = fetchJob->items();
+
+    if (keptItems.isEmpty()) {
+        // Urrrr? Not again!
+        doItemRemoval(item);
+    } else {
+        Akonadi::Item mainItem;
+        Akonadi::Item::List extraItems;
+        QString ridBase = item.remoteId();
+        ridBase.truncate(ridBase.indexOf(QLatin1Char('#')));
+
+        foreach (const Akonadi::Item &kept, keptItems) {
+            if (kept.remoteId() == ridBase && extraItems.isEmpty()) {
+                mainItem = kept;
+            } else {
+                extraItems << kept;
+            }
+        }
+
+        if (!mainItem.hasPayload()) {
+            mainItem = extraItems.takeFirst();
+        }
+
+        const DavUtils::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(item.parentCollection().remoteId(), ridBase);
+
+        DavItem davItem = DavUtils::createDavItem(mainItem, mainItem.parentCollection(), extraItems);
+        davItem.setUrl(ridBase);
+        davItem.setEtag(item.remoteRevision());
+
+        DavItemModifyJob *modJob = new DavItemModifyJob(davUrl, davItem);
+        modJob->setProperty("collection", QVariant::fromValue(mainItem.parentCollection()));
+        modJob->setProperty("item", QVariant::fromValue(mainItem));
+        modJob->setProperty("dependentItems", QVariant::fromValue(extraItems));
+        modJob->setProperty("isRemoval", QVariant::fromValue(true));
+        modJob->setProperty("removedItem", QVariant::fromValue(item));
+        connect(modJob, &DavItemModifyJob::result, this, &DavGroupwareResource::onItemChangedFinished);
+        modJob->start();
+    }
+}
+
+void DavGroupwareResource::doItemRemoval(const Akonadi::Item &item)
+{
+    const DavUtils::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(item.parentCollection().remoteId(), item.remoteId());
+
+    DavItem davItem;
+    davItem.setUrl(item.remoteId());
+    davItem.setEtag(item.remoteRevision());
+
+    DavItemDeleteJob *job = new DavItemDeleteJob(davUrl, davItem);
+    job->setProperty("item", QVariant::fromValue(item));
+    job->setProperty("collection", QVariant::fromValue(item.parentCollection()));
+    connect(job, &DavCollectionDeleteJob::result, this, &DavGroupwareResource::onItemRemovedFinished);
+    job->start();
+}
+
+void DavGroupwareResource::doSetOnline(bool online)
+{
+    qCDebug(DAVRESOURCE_LOG) << "Resource changed online status to" << online;
+
+    if (online) {
+        synchronize();
+    }
+
+    ResourceBase::doSetOnline(online);
+}
+
+void DavGroupwareResource::createInitialCache()
+{
+    // Get all the items fetched by this resource
+    Akonadi::RecursiveItemFetchJob *job = new Akonadi::RecursiveItemFetchJob(mDavCollectionRoot, QStringList());
+    job->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::Parent);
+    connect(job, &Akonadi::RecursiveItemFetchJob::result, this, &DavGroupwareResource::onCreateInitialCacheReady);
+    job->start();
+}
+
+void DavGroupwareResource::onCreateInitialCacheReady(KJob *job)
+{
+    Akonadi::RecursiveItemFetchJob *fetchJob = qobject_cast<Akonadi::RecursiveItemFetchJob *>(job);
+
+    foreach (const Akonadi::Item &item, fetchJob->items()) {
+        const QString rid = item.remoteId();
+        if (rid.isEmpty()) {
+            qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onCreateInitialCacheReady: Found an item without remote ID. " << item.id();
+            continue;
+        }
+
+        const Akonadi::Collection collection = item.parentCollection();
+        if (collection.remoteId().isEmpty()) {
+            qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onCreateInitialCacheReady: Found an item in a collection without remote ID. "
+                                     << item.remoteId();
+            continue;
+        }
+
+        const QString etag = item.remoteRevision();
+        if (etag.isEmpty()) {
+            qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onCreateInitialCacheReady: Found an item without ETag. " << item.remoteId();
+            continue;
+        }
+
+        if (!mEtagCaches.contains(collection.remoteId())) {
+            EtagCache *cache = new EtagCache(collection);
+            mEtagCaches.insert(collection.remoteId(), cache);
+        }
+
+        mEtagCaches[collection.remoteId()]->setEtag(rid, etag);
+    }
+
+    taskDone();
+}
+
+void DavGroupwareResource::onReloadConfig()
+{
+    Settings::self()->reloadConfig();
+    synchronize();
+}
+
+void DavGroupwareResource::onCollectionRemovedFinished(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(i18n("Unable to remove collection: %1", job->errorText()));
+        return;
+    }
+
+    Akonadi::Collection collection = job->property("collection").value<Akonadi::Collection>();
+
+    if (mEtagCaches.contains(collection.remoteId())) {
+        mEtagCaches[collection.remoteId()]->deleteLater();
+        mEtagCaches.remove(collection.remoteId());
+    }
+
+    changeProcessed();
+}
+
+void DavGroupwareResource::onRetrieveCollectionsFinished(KJob *job)
+{
+    const DavCollectionsMultiFetchJob *fetchJob = qobject_cast<DavCollectionsMultiFetchJob *>(job);
+
+    if (job->error()) {
+        qCWarning(DAVRESOURCE_LOG) << "Unable to fetch collections" << job->error() << job->errorText();
+        cancelTask(i18n("Unable to retrieve collections: %1", job->errorText()));
+        mSyncErrorNotified = true;
+        return;
+    }
+
+    Akonadi::Collection::List collections;
+    collections << mDavCollectionRoot;
+    QSet<QString> seenCollectionsUrls;
+
+    const DavCollection::List davCollections = fetchJob->collections();
+
+    foreach (const DavCollection &davCollection, davCollections) {
+        if (seenCollectionsUrls.contains(davCollection.url())) {
+            qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onRetrieveCollectionsFinished: Duplicate collection reported. " << davCollection.url();
+            continue;
+        } else {
+            seenCollectionsUrls.insert(davCollection.url());
+        }
+
+        Akonadi::Collection collection;
+        collection.setParentCollection(mDavCollectionRoot);
+        collection.setRemoteId(davCollection.url());
+        collection.setName(collection.remoteId());
+
+        if (!davCollection.displayName().isEmpty()) {
+            EntityDisplayAttribute *attr = collection.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
+            attr->setDisplayName(davCollection.displayName());
+        }
+
+        QStringList mimeTypes;
+        mimeTypes << Collection::mimeType();
+
+        const DavCollection::ContentTypes contentTypes = davCollection.contentTypes();
+        if (contentTypes & DavCollection::Calendar) {
+            mimeTypes << QStringLiteral("text/calendar");
+        }
+
+        if (contentTypes & DavCollection::Events) {
+            mimeTypes << KCalCore::Event::eventMimeType();
+        }
+
+        if (contentTypes & DavCollection::Todos) {
+            mimeTypes << KCalCore::Todo::todoMimeType();
+        }
+
+        if (contentTypes & DavCollection::Contacts) {
+            mimeTypes << KContacts::Addressee::mimeType();
+        }
+
+        if (contentTypes & DavCollection::FreeBusy) {
+            mimeTypes << KCalCore::FreeBusy::freeBusyMimeType();
+        }
+
+        if (contentTypes & DavCollection::Journal) {
+            mimeTypes << KCalCore::Journal::journalMimeType();
+        }
+
+        collection.setContentMimeTypes(mimeTypes);
+        setCollectionIcon(collection /*by-ref*/);
+
+        DavProtocolAttribute *protoAttr = collection.attribute<DavProtocolAttribute>(Collection::AddIfMissing);
+        protoAttr->setDavProtocol(davCollection.protocol());
+
+        /*
+         * We unfortunately have to update the CTag now in the cache
+         * as this information will not be available when retrieveItems()
+         * is called. We leave it untouched in the collection attribute
+         * and will only update it there after sucessfull sync.
+         */
+        if (!davCollection.CTag().isEmpty()) {
+            mCTagCache.insert(davCollection.url(), davCollection.CTag());
+        }
+
+        DavUtils::Privileges privileges = davCollection.privileges();
+        Akonadi::Collection::Rights rights;
+
+        if (privileges & DavUtils::All || privileges & DavUtils::Write) {
+            rights |= Akonadi::Collection::AllRights;
+        }
+
+        if (privileges & DavUtils::WriteContent) {
+            rights |= Akonadi::Collection::CanChangeItem;
+        }
+
+        if (privileges & DavUtils::Bind) {
+            rights |= Akonadi::Collection::CanCreateItem;
+        }
+
+        if (privileges & DavUtils::Unbind) {
+            rights |= Akonadi::Collection::CanDeleteItem;
+        }
+
+        if (privileges == DavUtils::Read) {
+            rights |= Akonadi::Collection::ReadOnly;
+        }
+
+        collection.setRights(rights);
+        collections << collection;
+
+        if (!mEtagCaches.contains(collection.remoteId())) {
+            EtagCache *cache = new EtagCache(collection);
+            mEtagCaches.insert(collection.remoteId(), cache);
+        }
+    }
+
+    foreach (const QString &rid, mEtagCaches.keys()) {
+        if (!seenCollectionsUrls.contains(rid)) {
+            qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onRetrieveCollectionsFinished: Collection disappeared. " << rid;
+            mEtagCaches[rid]->deleteLater();
+            mEtagCaches.remove(rid);
+        }
+    }
+
+    collectionsRetrieved(collections);
+}
+
+void DavGroupwareResource::onRetrieveItemsFinished(KJob *job)
+{
+    if (job->error()) {
+        if (mSyncErrorNotified) {
+            cancelTask();
+        } else {
+            cancelTask(i18n("Unable to retrieve items: %1", job->errorText()));
+            mSyncErrorNotified = true;
+        }
+        return;
+    }
+
+    Collection collection = job->property("collection").value<Collection>();
+    const DavUtils::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(collection.remoteId());
+    const bool protocolSupportsMultiget = DavManager::self()->davProtocol(davUrl.protocol())->useMultiget();
+
+    const DavItemsListJob *listJob = qobject_cast<DavItemsListJob *>(job);
+    EtagCache *cache = mEtagCaches.value(collection.remoteId());
+
+    Akonadi::Item::List changedItems;
+    QSet<QString> seenRids;
+    QStringList changedRids;
+
+    foreach (const DavItem &davItem, listJob->changedItems()) {
+        seenRids.insert(davItem.url());
+
+        Akonadi::Item item;
+        item.setParentCollection(collection);
+        item.setRemoteId(davItem.url());
+        item.setMimeType(davItem.contentType());
+        item.setRemoteRevision(davItem.etag());
+
+        cache->markAsChanged(item.remoteId());
+        changedRids << item.remoteId();
+        changedItems << item;
+
+        // Only clear the payload (and therefor trigger a refetch from the backend) if we
+        // do not use multiget, because in this case we fetch the complete payload
+        // some lines below already.
+        if (!protocolSupportsMultiget) {
+            qCDebug(DAVRESOURCE_LOG) << "Outdated item " << item.remoteId() << " (etag = " << davItem.etag() << ")";
+            item.clearPayload();
+        }
+    }
+
+    foreach (const QString &rmd, listJob->deletedItems()) {
+        // We don't want to delete dependent items if the main item was seen
+        if (rmd.contains(QLatin1Char('#'))) {
+            const QString base = rmd.left(rmd.indexOf(QLatin1Char('#')));
+            if (seenRids.contains(base)) {
+                continue;
+            }
+        }
+
+        qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onRetrieveItemsFinished: Item disappeared. " << rmd;
+        Akonadi::Item item;
+        item.setParentCollection(collection);
+        item.setRemoteId(rmd);
+        cache->removeEtag(rmd);
+
+        // Use a job to delete items as itemsRetrievedIncremental seem to choke
+        // when many items are given with just their RID.
+        Akonadi::ItemDeleteJob *deleteJob = new Akonadi::ItemDeleteJob(item);
+        deleteJob->start();
+    }
+
+    // If the protocol supports multiget then deviate from the expected behavior
+    // and fetch all items with payload now instead of waiting for Akonadi to
+    // request it item by item in retrieveItem().
+    // This allows the resource to use the multiget query and let it be nice
+    // to the remote server : only one request for n items instead of n requests.
+    if (protocolSupportsMultiget && !changedRids.isEmpty()) {
+        DavItemsFetchJob *fetchJob = new DavItemsFetchJob(davUrl, changedRids);
+        connect(fetchJob, &DavItemsFetchJob::result, this, &DavGroupwareResource::onMultigetFinished);
+        fetchJob->setProperty("collection", QVariant::fromValue(collection));
+        fetchJob->setProperty("items", QVariant::fromValue(changedItems));
+        fetchJob->start();
+        // delay the call of itemsRetrieved() to onMultigetFinished()
+    } else {
+        // Update the collection CTag attribute now as sync is done.
+        if (mCTagCache.contains(collection.remoteId())) {
+            CTagAttribute *CTagAttr = collection.attribute<CTagAttribute>(Collection::AddIfMissing);
+            qCDebug(DAVRESOURCE_LOG) << "Updating collection CTag from" << CTagAttr->CTag() << "to" << mCTagCache.value(collection.remoteId());
+            CTagAttr->setCTag(mCTagCache.value(collection.remoteId()));
+            Akonadi::CollectionModifyJob *job = new Akonadi::CollectionModifyJob(collection);
+            job->start();
+        }
+
+        itemsRetrievedIncremental(changedItems, Akonadi::Item::List());
+    }
+}
+
+void DavGroupwareResource::onMultigetFinished(KJob *job)
+{
+    if (job->error()) {
+        if (mSyncErrorNotified) {
+            cancelTask();
+        } else {
+            cancelTask(i18n("Unable to retrieve items: %1", job->errorText()));
+            mSyncErrorNotified = true;
+        }
+        return;
+    }
+
+    Akonadi::Collection collection = job->property("collection").value<Akonadi::Collection>();
+    EtagCache *cache = mEtagCaches.value(collection.remoteId());
+    const Akonadi::Item::List origItems = job->property("items").value<Akonadi::Item::List>();
+    const DavItemsFetchJob *davJob = qobject_cast<DavItemsFetchJob *>(job);
+
+    Akonadi::Item::List items;
+    foreach (Akonadi::Item item, origItems) {   //krazy:exclude=foreach non-const is intended here
+        const DavItem davItem = davJob->item(item.remoteId());
+
+        // No data was retrieved for this item, maybe because it is not out of date
+        if (davItem.data().isEmpty()) {
+            qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onMultigetFinished: Empty item returned. " << item.remoteId();
+            if (!cache->isOutOfDate(item.remoteId())) {
+                qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onMultigetFinished: Item is not changed, including it. " << item.remoteId();
+                items << item;
+            }
+            continue;
+        }
+
+        Akonadi::Item::List extraItems;
+        if (!DavUtils::parseDavData(davItem, item, extraItems)) {
+            qCWarning(DAVRESOURCE_LOG) << "DavGroupwareResource::onMultigetFinished: Failed to parse item data. " << item.remoteId();
+            continue;
+        }
+
+        // update etag
+        item.setRemoteRevision(davItem.etag());
+        cache->setEtag(item.remoteId(), davItem.etag());
+        items << item;
+        foreach (const Akonadi::Item &extraItem, extraItems) {
+            cache->setEtag(extraItem.remoteId(), davItem.etag());
+            items << extraItem;
+        }
+    }
+
+    if (mCTagCache.contains(collection.remoteId())) {
+        // Update the collection CTag attribute now as sync is done.
+        if (mCTagCache.contains(collection.remoteId())) {
+            CTagAttribute *CTagAttr = collection.attribute<CTagAttribute>(Collection::AddIfMissing);
+            qCDebug(DAVRESOURCE_LOG) << "Updating collection CTag from" << CTagAttr->CTag() << "to" << mCTagCache.value(collection.remoteId());
+            CTagAttr->setCTag(mCTagCache.value(collection.remoteId()));
+            Akonadi::CollectionModifyJob *job = new Akonadi::CollectionModifyJob(collection);
+            job->start();
+        }
+    }
+
+    itemsRetrievedIncremental(items, Akonadi::Item::List());
+}
+
+void DavGroupwareResource::onRetrieveItemFinished(KJob *job)
+{
+    onItemFetched(job, ItemUpdateAdd);
+}
+
+void DavGroupwareResource::onItemRefreshed(KJob *job)
+{
+    ItemFetchUpdateType update = ItemUpdateChange;
+    if (job->property("isRemoval").isValid() && job->property("isRemoval").toBool()) {
+        update = ItemUpdateNone;
+    }
+
+    onItemFetched(job, update);
+}
+
+void DavGroupwareResource::onItemFetched(KJob *job, ItemFetchUpdateType updateType)
+{
+    if (job->error()) {
+        if (mSyncErrorNotified) {
+            cancelTask();
+        } else {
+            cancelTask(i18n("Unable to retrieve item: %1", job->errorText()));
+            mSyncErrorNotified = true;
+        }
+        return;
+    }
+
+    const DavItemFetchJob *fetchJob = qobject_cast<DavItemFetchJob *>(job);
+    const DavItem davItem = fetchJob->item();
+    Akonadi::Item item = fetchJob->property("item").value<Akonadi::Item>();
+    Akonadi::Collection collection = fetchJob->property("collection").value<Akonadi::Collection>();
+
+    Akonadi::Item::List extraItems;
+    if (!DavUtils::parseDavData(davItem, item, extraItems)) {
+        qCWarning(DAVRESOURCE_LOG) << "DavGroupwareResource::onItemFetched: Failed to parse item data. " << item.remoteId();
+        return;
+    }
+
+    // update etag
+    item.setRemoteRevision(davItem.etag());
+    EtagCache *etag = mEtagCaches[collection.remoteId()];
+    etag->setEtag(item.remoteId(), davItem.etag());
+
+    if (!extraItems.isEmpty()) {
+        for (int i = 0; i < extraItems.size(); ++i) {
+            etag->setEtag(extraItems.at(i).remoteId(), davItem.etag());
+        }
+
+        Akonadi::ItemModifyJob *j = new Akonadi::ItemModifyJob(extraItems);
+        j->setIgnorePayload(true);
+    }
+
+    if (updateType == ItemUpdateChange) {
+        changeCommitted(item);
+    } else if (updateType == ItemUpdateAdd) {
+        itemRetrieved(item);
+    }
+}
+
+void DavGroupwareResource::onItemAddedFinished(KJob *job)
+{
+    const DavItemCreateJob *createJob = qobject_cast<DavItemCreateJob *>(job);
+    const DavItem davItem = createJob->item();
+    Akonadi::Item item = createJob->property("item").value<Akonadi::Item>();
+    item.setRemoteId(davItem.url());
+
+    if (createJob->error()) {
+        qCCritical(DAVRESOURCE_LOG) << "Error when uploading item:" << createJob->error() << createJob->errorString();
+        if (createJob->canRetryLater()) {
+            retryAfterFailure(createJob->errorString());
+        } else {
+            cancelTask(i18n("Unable to add item: %1", createJob->errorString()));
+        }
+        return;
+    }
+
+    Akonadi::Collection collection = createJob->property("collection").value<Akonadi::Collection>();
+
+    if (davItem.etag().isEmpty()) {
+        const DavUtils::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(collection.remoteId(), item.remoteId());
+        DavItemFetchJob *fetchJob = new DavItemFetchJob(davUrl, davItem);
+        fetchJob->setProperty("item", QVariant::fromValue(item));
+        fetchJob->setProperty("collection", QVariant::fromValue(collection));
+        connect(fetchJob, &DavItemFetchJob::result, this, &DavGroupwareResource::onItemRefreshed);
+        fetchJob->start();
+    } else {
+        item.setRemoteRevision(davItem.etag());
+        mEtagCaches[collection.remoteId()]->setEtag(davItem.url(), davItem.etag());
+        changeCommitted(item);
+    }
+}
+
+void DavGroupwareResource::onItemChangedFinished(KJob *job)
+{
+    const DavItemModifyJob *modifyJob = qobject_cast<DavItemModifyJob *>(job);
+    const DavItem davItem = modifyJob->item();
+    Akonadi::Collection collection = modifyJob->property("collection").value<Akonadi::Collection>();
+    Akonadi::Item item = modifyJob->property("item").value<Akonadi::Item>();
+    Akonadi::Item::List dependentItems = modifyJob->property("dependentItems").value<Akonadi::Item::List>();
+    bool isRemoval = modifyJob->property("isRemoval").isValid() && modifyJob->property("isRemoval").toBool();
+    EtagCache *cache = mEtagCaches.value(collection.remoteId());
+
+    if (modifyJob->error()) {
+        qCCritical(DAVRESOURCE_LOG) << "Error when uploading item:" << modifyJob->error() << modifyJob->errorString();
+        if (modifyJob->hasConflict()) {
+            handleConflict(item, dependentItems, modifyJob->freshItem(), isRemoval, modifyJob->freshResponseCode());
+        } else if (modifyJob->canRetryLater()) {
+            retryAfterFailure(modifyJob->errorString());
+        } else {
+            cancelTask(i18n("Unable to change item: %1", modifyJob->errorString()));
+        }
+        return;
+    }
+
+    if (isRemoval) {
+        Akonadi::Item removedItem = job->property("removedItem").value<Akonadi::Item>();
+        cache->removeEtag(removedItem.remoteId());
+        changeProcessed();
+    }
+
+    if (davItem.etag().isEmpty()) {
+        const DavUtils::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(item.parentCollection().remoteId(), item.remoteId());
+        DavItemFetchJob *fetchJob = new DavItemFetchJob(davUrl, davItem);
+        fetchJob->setProperty("item", QVariant::fromValue(item));
+        fetchJob->setProperty("collection", QVariant::fromValue(collection));
+        fetchJob->setProperty("dependentItems", QVariant::fromValue(dependentItems));
+        fetchJob->setProperty("isRemoval", QVariant::fromValue(isRemoval));
+        connect(fetchJob, &DavItemsFetchJob::result, this, &DavGroupwareResource::onItemRefreshed);
+        fetchJob->start();
+    } else {
+        if (!isRemoval) {
+            item.setRemoteRevision(davItem.etag());
+            cache->setEtag(davItem.url(), davItem.etag());
+            changeCommitted(item);
+        }
+
+        if (!dependentItems.isEmpty()) {
+            for (int i = 0; i < dependentItems.size(); ++i) {
+                dependentItems[i].setRemoteRevision(davItem.etag());
+                cache->setEtag(dependentItems.at(i).remoteId(), davItem.etag());
+            }
+
+            Akonadi::ItemModifyJob *j = new Akonadi::ItemModifyJob(dependentItems);
+            j->setIgnorePayload(true);
+        }
+    }
+}
+
+void DavGroupwareResource::onDeletedItemRecreated(KJob *job)
+{
+    const DavItemCreateJob *createJob = qobject_cast<DavItemCreateJob *>(job);
+    const DavItem davItem = createJob->item();
+    Akonadi::Item item = createJob->property("item").value<Akonadi::Item>();
+    Akonadi::Collection collection = item.parentCollection();
+    Akonadi::Item::List dependentItems = createJob->property("dependentItems").value<Akonadi::Item::List>();
+
+    if (davItem.etag().isEmpty()) {
+        const DavUtils::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(item.parentCollection().remoteId(), item.remoteId());
+        DavItemFetchJob *fetchJob = new DavItemFetchJob(davUrl, davItem);
+        fetchJob->setProperty("item", QVariant::fromValue(item));
+        fetchJob->setProperty("dependentItems", QVariant::fromValue(dependentItems));
+        connect(fetchJob, &DavItemFetchJob::result, this, &DavGroupwareResource::onItemRefreshed);
+        fetchJob->start();
+    } else {
+        item.setRemoteRevision(davItem.etag());
+        EtagCache *etag = mEtagCaches[collection.remoteId()];
+        etag->setEtag(davItem.url(), davItem.etag());
+        changeCommitted(item);
+
+        if (!dependentItems.isEmpty()) {
+            for (int i = 0; i < dependentItems.size(); ++i) {
+                dependentItems[i].setRemoteRevision(davItem.etag());
+                etag->setEtag(dependentItems.at(i).remoteId(), davItem.etag());
+            }
+
+            Akonadi::ItemModifyJob *j = new Akonadi::ItemModifyJob(dependentItems);
+            j->setIgnorePayload(true);
+        }
+    }
+}
+
+void DavGroupwareResource::onItemRemovedFinished(KJob *job)
+{
+    if (job->error()) {
+        const DavItemDeleteJob *deleteJob = qobject_cast<DavItemDeleteJob *>(job);
+
+        if (deleteJob->hasConflict()) {
+            // Use a shortcut here as we don't show a conflict dialog to the user.
+            handleConflict(Akonadi::Item(), Akonadi::Item::List(), deleteJob->freshItem(), true, 0);
+        } else if (deleteJob->canRetryLater()) {
+            retryAfterFailure(job->errorString());
+        } else {
+            cancelTask(i18n("Unable to remove item: %1", job->errorString()));
+        }
+    } else {
+        Akonadi::Item item = job->property("item").value<Akonadi::Item>();
+        Akonadi::Collection collection = job->property("collection").value<Akonadi::Collection>();
+        mEtagCaches[collection.remoteId()]->removeEtag(item.remoteId());
+        changeProcessed();
+    }
+}
+
+void DavGroupwareResource::onCollectionDiscovered(int protocol, const QString &collection, const QString &config)
+{
+    Settings::self()->addCollectionUrlMapping(DavUtils::Protocol(protocol), collection, config);
+}
+
+void DavGroupwareResource::handleConflict(const Item &lI, const Item::List &localDependentItems, const DavItem &rI, bool isLocalRemoval, int responseCode)
+{
+    Akonadi::Item localItem(lI);
+    Akonadi::Item remoteItem, tmpRemoteItem;                           // The tmp* vars are here to store the result of the parseDavData() call
+    Akonadi::Item::List remoteDependentItems, tmpRemoteDependentItems; // as we have no idea which item triggered the conflict.
+    qCDebug(DAVRESOURCE_LOG) << "Fresh response code is" << responseCode;
+    bool isRemoteRemoval = (responseCode == 404 || responseCode == 410);
+
+    if (!isRemoteRemoval) {
+        if (!DavUtils::parseDavData(rI, tmpRemoteItem, tmpRemoteDependentItems)) {
+            // TODO: set a more correct error message here
+            cancelTask(i18n("Unable to change item: %1", QStringLiteral("conflict resolution failed")));
+            return;
+            // TODO: we can end up here if the remote item was deleted
+        }
+
+        // Now try to find the item that really triggered the conflict
+        Akonadi::Item::List allRemoteItems; allRemoteItems << tmpRemoteItem << tmpRemoteDependentItems;
+        foreach (const Akonadi::Item &tmpItem, allRemoteItems) {
+            if (tmpItem.payloadData() != localItem.payloadData()) {
+                if (remoteItem.isValid()) {
+                    // Oops, we can only manage one changed item at this stage, sorry...
+                    // TODO: make this translatable
+                    cancelTask(i18n("Unable to change item: %1", QStringLiteral("more than one item was changed in the backend")));
+                    return;
+                }
+                remoteItem = tmpItem;
+            } else {
+                remoteDependentItems << tmpItem;
+            }
+        }
+    }
+
+    if (isLocalRemoval) {
+        // TODO: implement with the configurable strategy
+        /*
+         * Here by default we don't delete an event that was modified in the backend, and
+         * instead we just abort the current task.
+         * Also, trigger an immediate sync to refresh the item.
+         */
+        qCDebug(DAVRESOURCE_LOG) << "Local removal conflict";
+        // TODO: make this translatable
+        cancelTask(i18n("Unable to remove item: %1", QStringLiteral("it was changed in the backend in the meantime")));
+        synchronize();
+    } else if (isRemoteRemoval) {
+        // TODO: implement with the configurable strategy
+        /*
+         * Here also it is a bit tricky to clear the item in the local cache as the resource
+         * will not get notified if the user chooses to delete the item and abandon the local
+         * modification. For the time being let's just re-upload the changed item.
+         */
+        qCDebug(DAVRESOURCE_LOG) << "Remote removal conflict";
+        Akonadi::Collection collection = localItem.parentCollection();
+        DavItem davItem = DavUtils::createDavItem(localItem, collection, localDependentItems);
+
+        QString urlStr = localItem.remoteId();
+        if (urlStr.contains(QLatin1Char('#'))) {
+            urlStr.truncate(urlStr.indexOf(QLatin1Char('#')));
+        }
+        davItem.setUrl(urlStr);
+        const DavUtils::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(collection.remoteId(), urlStr);
+
+        DavItemCreateJob *job = new DavItemCreateJob(davUrl, davItem);
+        job->setProperty("item", QVariant::fromValue(localItem));
+        job->setProperty("dependentItems", QVariant::fromValue(localDependentItems));
+        connect(job, &KJob::result, this, &DavGroupwareResource::onDeletedItemRecreated);
+        job->start();
+    } else {
+        const QString remoteEtag = rI.etag();
+        Akonadi::Collection collection = localItem.parentCollection();
+
+        localItem.setRemoteRevision(remoteEtag);
+        changeCommitted(localItem);
+
+        // Update the ETag cache in all cases as the new ETag will have to be used
+        // later for any update or deletion
+        mEtagCaches[collection.remoteId()]->setEtag(rI.url(), remoteEtag);
+
+        // The first step is to fire a first modify job that will replace the item currently
+        // in the local cache with the one that was found in the backend.
+        Akonadi::Item updatedItem(localItem);
+        updatedItem.setPayloadFromData(remoteItem.payloadData());
+        updatedItem.setRemoteRevision(remoteEtag);
+        Akonadi::ItemModifyJob *j = new Akonadi::ItemModifyJob(updatedItem);
+        j->setIgnorePayload(false);
+        j->start();
+
+        // So now we have in the cache what's in the backend but the user is not aware
+        // that behind the scenes something terrible is happening. Well, nearly...
+        // To notify him of this, and due to the way the conflict handler works, we have
+        // to re-attempt a modification to revert the modify job that was just fired.
+        // So yes, we are effectively re-submitting the client-provided content, but
+        // with a revision that will trigger the conflict dialog.
+        // The only problem is that the user will see that we update the item before
+        // the conflict dialog has time to display (if it's not behind the application
+        // window).
+        localItem.setRevision(0);
+        j = new Akonadi::ItemModifyJob(localItem);
+        j->setIgnorePayload(false);
+        connect(j, &KJob::result, this, &DavGroupwareResource::onConflictModifyJobFinished);
+        j->start();
+
+        // Hopefully for the dependent items everything will be fine. Right?
+        // Not so sure in fact.
+        if (!remoteDependentItems.isEmpty()) {
+            EtagCache *etag = mEtagCaches[collection.remoteId()];
+            for (int i = 0; i < remoteDependentItems.size(); ++i) {
+                remoteDependentItems[i].setRemoteRevision(remoteEtag);
+                etag->setEtag(remoteDependentItems.at(i).remoteId(), remoteEtag);
+            }
+
+            Akonadi::ItemModifyJob *j = new Akonadi::ItemModifyJob(remoteDependentItems);
+            j->setIgnorePayload(true);
+        }
+    }
+}
+
+void DavGroupwareResource::onConflictModifyJobFinished(KJob *job)
+{
+    Akonadi::ItemModifyJob *j = qobject_cast<Akonadi::ItemModifyJob *>(job);
+    if (j->error()) {
+        qCCritical(DAVRESOURCE_LOG) << "Conflict update failed: " << job->errorText();
+        // TODO: what do we do now? We just committed an item that's in a weird state...
+    }
+}
+
+bool DavGroupwareResource::configurationIsValid()
+{
+    if (Settings::self()->configuredDavUrls().empty()) {
+        Q_EMIT status(NotConfigured, i18n("The resource is not configured yet"));
+        cancelTask(i18n("The resource is not configured yet"));
+        return false;
+    }
+
+    int newICT = Settings::self()->refreshInterval();
+    if (newICT == 0) {
+        newICT = -1;
+    }
+
+    if (newICT != mDavCollectionRoot.cachePolicy().intervalCheckTime()) {
+        Akonadi::CachePolicy cachePolicy = mDavCollectionRoot.cachePolicy();
+        cachePolicy.setIntervalCheckTime(newICT);
+
+        mDavCollectionRoot.setCachePolicy(cachePolicy);
+    }
+
+    if (!Settings::self()->displayName().isEmpty()) {
+        EntityDisplayAttribute *attribute = mDavCollectionRoot.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
+        attribute->setDisplayName(Settings::self()->displayName());
+        setName(Settings::self()->displayName());
+    }
+
+    return true;
+}
+
+void DavGroupwareResource::retryAfterFailure(const QString &errorMessage)
+{
+    Q_EMIT status(Broken, errorMessage);
+    deferTask();
+    setTemporaryOffline(Settings::self()->refreshInterval() <= 0 ? 300 : Settings::self()->refreshInterval() * 60);
+}
+
+/*static*/
+void DavGroupwareResource::setCollectionIcon(Akonadi::Collection &collection)
+{
+    const QStringList mimeTypes = collection.contentMimeTypes();
+    if (mimeTypes.count() == 1) {
+        QHash<QString, QString> mapping;
+        mapping.insert(KCalCore::Event::eventMimeType(), QStringLiteral("view-calendar"));
+        mapping.insert(KCalCore::Todo::todoMimeType(), QStringLiteral("view-calendar-tasks"));
+        mapping.insert(KCalCore::Journal::journalMimeType(), QStringLiteral("view-pim-journal"));
+        mapping.insert(KContacts::Addressee::mimeType(), QStringLiteral("view-pim-contacts"));
+
+        if (mapping.contains(mimeTypes.first())) {
+            EntityDisplayAttribute *attribute = collection.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
+            attribute->setIconName(mapping.value(mimeTypes.first()));
+        }
+    }
+}
+
+AKONADI_RESOURCE_MAIN(DavGroupwareResource)
+
diff --git a/resources/dav/resource/davgroupwareresource.desktop b/resources/dav/resource/davgroupwareresource.desktop
new file mode 100644 (file)
index 0000000..4ebbf6a
--- /dev/null
@@ -0,0 +1,90 @@
+[Desktop Entry]
+Name=DAV groupware resource
+Name[bs]=DAV grupni resurs
+Name[ca]=Recurs de treball en grup DAV
+Name[ca@valencia]=Recurs de treball en grup DAV
+Name[da]=Ressource til DAV-groupware
+Name[de]=DAV-Groupware-Ressource
+Name[el]=Πόρος DAV groupware
+Name[en_GB]=DAV groupware resource
+Name[es]=Recurso de colaboración DAV
+Name[et]=DAV grupitöö ressurss
+Name[fi]=DAV-työryhmäresurssi
+Name[fr]=Ressource pour logiciels de collaboration DAV
+Name[ga]=Acmhainn groupware DAV
+Name[gl]=Servidor de traballo en grupo DAV
+Name[hu]=DAV csoportmunka-erőforrás
+Name[ia]=Ressource de DAV Groupware
+Name[it]=Risorsa di groupware DAV
+Name[ja]=DAV グループウェアリソース
+Name[kk]=DAV топтық ресурсы
+Name[km]=ធនធាន DAV groupware
+Name[ko]=DAV 그룹웨어 자원
+Name[lt]=DAV grupinės įrangos resursai
+Name[lv]=DAV grupprogrammatūras resurss
+Name[nb]=DAV Groupware-ressurs
+Name[nds]=DAV-Arbeitkoppel-Ressource
+Name[nl]=DAV-groupware-hulpbron
+Name[pa]=DAV ਗਰੁੱਪਵੇਅਰ ਸਰੋਤ
+Name[pl]=Zasób groupware DAV
+Name[pt]=Recurso de 'groupware' em DAV
+Name[pt_BR]=Recurso de groupware por DAV
+Name[ru]=Источник данных совместной работы DAV
+Name[sk]=Zdroj DAV groupware
+Name[sl]=Vir za skupinsko delo DAV
+Name[sr]=Групверски ДАВ ресурс
+Name[sr@ijekavian]=Групверски ДАВ ресурс
+Name[sr@ijekavianlatin]=Grupverski DAV resurs
+Name[sr@latin]=Grupverski DAV resurs
+Name[sv]=DAV-grupprogramresurs
+Name[tr]=DAV groupware kaynağı
+Name[uk]=Ресурс групової роботи DAV
+Name[x-test]=xxDAV groupware resourcexx
+Name[zh_CN]=DAV 群件资源
+Name[zh_TW]=DAV 群組資源
+Comment="Resource to manage DAV calendars and address books (CalDAV, GroupDAV)"
+Comment[bs]="Resurs za upravaljanje DAV kalendarima i adresarima (CalDAV, GroupDAV)"
+Comment[ca]=«Recurs per gestionar calendaris i llibretes d'adreces DAV (CalDAV, GroupDAV)»
+Comment[ca@valencia]=«Recurs per gestionar calendaris i llibretes d'adreces DAV (CalDAV, GroupDAV)»
+Comment[da]="Ressource til håndtering af DAV-kalendere og -adressebøger (CalDAV, GroupDAV)"
+Comment[de]="Ressource zur Verwaltung von DAV-Kalendern und -Adressbüchern (CalDAV, GroupDAV)"
+Comment[el]="Πόρος για τη διαχείριση ημερολογίων DAV και βιβλίων διευθύνσεων (CalDAV, GroupDAV)"
+Comment[en_GB]="Resource to manage DAV calendars and address books (CalDAV, GroupDAV)"
+Comment[es]=«Recurso para gestionar calendarios DAV y libretas de direcciones (CalDAV, GroupDAV)»
+Comment[et]="Ressurss DAV-kalendrite ja aadressiraamatute (CalDAV, GroupDAV) haldamiseks"
+Comment[fi]="DAV-kalenterien ja -osoitekirjojen (CalDAV, GroupDAV) hallintaresurssi"
+Comment[fr]=« Ressource pour gérer les agendas et carnets d'adresses DAV (CalDAV, GroupDAV) »
+Comment[gl]=«Recurso para xestionar os calendarios e cadernos de enderezos DAV (CalDAV, GroupDAV)»
+Comment[hu]=„Erőforrás DAV naptárak és címjegyzékek (CalDAV, GroupDAV) kezeléséhez”
+Comment[ia]="Ressource pro administrar calendarios e adressarios de DAV (CalDAV, GroupDAV)"
+Comment[it]="Risorsa per gestire calendari e rubriche DAV (CalDAV, GroupDAV)"
+Comment[kk]="DAV күнтізбе және адрестік кітапшасын (CalDAV, GroupDAV) басқару ресурсы"
+Comment[ko]="DAV 달력과 주소록을 관리하는 자원 (CalDAV, GroupDAV)"
+Comment[lt]=„Resursas, DAV kalendorių ir adresų knygų, valdymui (CalDAV, GroupDAV)“
+Comment[nb]=«Ressurs som håndterer DAV-kalendere og adressebøker (CalDAV, GroupDAV)»
+Comment[nds]=„Ressource för de Pleeg vun DAV-Kalenners un -Adressböker (CalDAV, GroupDAV)“
+Comment[nl]="Hulpbron om DAV-agenda's en adresboeken (CalDAV, GroupDAV) te beheren"
+Comment[pl]="Zasób do obsługi kalendarzy i książek adresowych DAV (CalDAV, GroupDAV)"
+Comment[pt]=Recurso para gerir os calendários e livros de endereços em DAV (CalDAV, GroupDAV)
+Comment[pt_BR]="Recurso para gerenciar calendários e livros de endereços por DAV (CalDAV, GroupDAV)"
+Comment[ru]="Источники данных для управления календарями и адресными книгами DAV (CalDAV, GroupDAV)"
+Comment[sk]="Zdroj na správu DAV kalendárov a adresárov kontaktov (CalDAV, GroupDAV)"
+Comment[sl]=»Vir za upravljanje koledarjev in imenikov DAV (CalDAV, GroupDAV)«
+Comment[sr]="Ресурс за управљање ДАВ календарима и адресарима (калДАВ, групДАВ)"
+Comment[sr@ijekavian]="Ресурс за управљање ДАВ календарима и адресарима (калДАВ, групДАВ)"
+Comment[sr@ijekavianlatin]="Resurs za upravljanje DAV kalendarima i adresarima (CalDAV, GroupDAV)"
+Comment[sr@latin]="Resurs za upravljanje DAV kalendarima i adresarima (CalDAV, GroupDAV)"
+Comment[sv]="Resurs för att hantera DAV-kalendrar och adressböcker (CalDAV, GroupDAV)"
+Comment[tr]="DAV takvim ve adres defterlerini yönetmek için kaynak (CalDAV, GroupDAV)"
+Comment[uk]="Ресурс для керування календарями і адресними книгами DAV (CalDAV, GroupDAV)"
+Comment[x-test]=xx"Resource to manage DAV calendars and address books (CalDAV, GroupDAV)"xx
+Comment[zh_CN]=“管理 DAV 日历和地址簿的资源 (CalDAV、GroupDAV)”
+Comment[zh_TW]="管理 DAV 行事曆與通訊錄的資源(CalDAV, GroupDAV)"
+Type=AkonadiResource
+Exec=akonadi_davgroupware_resource
+Icon=folder-remote
+
+X-Akonadi-Custom-KAccounts=dav-calendar,dav-contacts
+X-Akonadi-MimeTypes=text/calendar,application/x-vnd.akonadi.calendar.event,application/x-vnd.akonadi.calendar.todo,application/x-vnd.akonadi.calendar.journal,application/x-vnd.akonadi.calendar.freebusy,text/directory
+X-Akonadi-Capabilities=Resource,FreeBusyProvider
+X-Akonadi-Identifier=akonadi_davgroupware_resource
diff --git a/resources/dav/resource/davgroupwareresource.h b/resources/dav/resource/davgroupwareresource.h
new file mode 100644 (file)
index 0000000..05e2c10
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+    Copyright (c) 2009 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVGROUPWARERESOURCE_H
+#define DAVGROUPWARERESOURCE_H
+
+#include "etagcache.h"
+
+#include <resourcebase.h>
+#include <akonadi/calendar/freebusyproviderbase.h>
+
+class DavFreeBusyHandler;
+class DavItem;
+class KDateTime;
+
+#include <QtCore/QSet>
+#include <QtCore/QStringList>
+
+class DavGroupwareResource : public Akonadi::ResourceBase,
+    public Akonadi::AgentBase::Observer,
+    public Akonadi::FreeBusyProviderBase
+{
+    Q_OBJECT
+
+public:
+    explicit DavGroupwareResource(const QString &id);
+    ~DavGroupwareResource();
+
+    void collectionRemoved(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    void cleanup() Q_DECL_OVERRIDE;
+
+    KDateTime lastCacheUpdate() const Q_DECL_OVERRIDE;
+    void canHandleFreeBusy(const QString &email) const Q_DECL_OVERRIDE;
+    void retrieveFreeBusy(const QString &email, const KDateTime &start, const KDateTime &end) Q_DECL_OVERRIDE;
+
+public Q_SLOTS:
+    void configure(WId windowId) Q_DECL_OVERRIDE;
+
+protected Q_SLOTS:
+    void retrieveCollections() Q_DECL_OVERRIDE;
+    void retrieveItems(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    bool retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+
+protected:
+    void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    void itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+    void itemRemoved(const Akonadi::Item &item) Q_DECL_OVERRIDE;
+    void doSetOnline(bool online) Q_DECL_OVERRIDE;
+
+private:
+    enum ItemFetchUpdateType {
+        ItemUpdateNone,
+        ItemUpdateAdd,
+        ItemUpdateChange
+    };
+
+private Q_SLOTS:
+    void createInitialCache();
+    void onCreateInitialCacheReady(KJob *);
+
+    void onReloadConfig();
+    void onCollectionRemovedFinished(KJob *);
+
+    void onHandlesFreeBusy(const QString &email, bool handles);
+    void onFreeBusyRetrieved(const QString &email, const QString &freeBusy, bool success, const QString &errorText);
+
+    void onRetrieveCollectionsFinished(KJob *);
+    void onRetrieveItemsFinished(KJob *);
+    void onMultigetFinished(KJob *);
+    void onRetrieveItemFinished(KJob *);
+    /**
+      * Called when a new item has been fetched from the backend.
+      *
+      * @param job The job that fetched the item
+      * @param updateType The type of update that triggered this call. The task notification sent
+      *        sent to Akonadi will depend on this flag.
+      */
+    void onItemFetched(KJob *job, ItemFetchUpdateType updateType);
+    void onItemRefreshed(KJob *job);
+
+    void onItemAddedFinished(KJob *);
+    void onItemChangePrepared(KJob *);
+    void onItemChangedFinished(KJob *);
+    void onItemRemovalPrepared(KJob *);
+    void onItemRemovedFinished(KJob *);
+
+    void onCollectionDiscovered(int protocol, const QString &collectionUrl, const QString &configuredUrl);
+    void onConflictModifyJobFinished(KJob *job);
+    void onDeletedItemRecreated(KJob *job);
+
+private:
+    void doItemChange(const Akonadi::Item &item, const Akonadi::Item::List &dependentItems = Akonadi::Item::List());
+    void doItemRemoval(const Akonadi::Item &item);
+    void handleConflict(const Akonadi::Item &localItem,
+                        const Akonadi::Item::List &localDependentItems,
+                        const DavItem &remoteItem,
+                        bool isLocalRemoval,
+                        int responseCode
+                       );
+
+    bool configurationIsValid();
+    void retryAfterFailure(const QString &errorMessage);
+
+    /**
+     * Collections which only support one mime type have an icon indicating what they support.
+     */
+    static void setCollectionIcon(Akonadi::Collection &collection);
+
+    Akonadi::Collection mDavCollectionRoot;
+    QMap<QString, EtagCache *> mEtagCaches;
+    QMap<QString, QString> mCTagCache;
+    DavFreeBusyHandler *mFreeBusyHandler;
+    bool mSyncErrorNotified;
+};
+
+#endif
diff --git a/resources/dav/resource/davgroupwareresource.kcfg b/resources/dav/resource/davgroupwareresource.kcfg
new file mode 100644 (file)
index 0000000..35804dc
--- /dev/null
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:kcfg="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
+      http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
+  <kcfgfile/>
+  <group name="General">
+    <entry name="settingsVersion" type="Int">
+      <label>Settings Version</label>
+      <default>1</default>
+    </entry>
+    <entry name="displayName" type="String">
+      <label>Display name</label>
+      <default></default>
+    </entry>
+    <entry name="refreshInterval" type="Int">
+      <label>Refresh every</label>
+      <default>5</default>
+    </entry>
+    <entry name="remoteUrls" type="StringList">
+      <label>Remote URLs</label>
+      <default></default>
+    </entry>
+    <entry name="defaultUsername" type="String">
+      <label>User name</label>
+      <default></default>
+    </entry>
+    <entry name="limitSyncRange" type="Bool">
+      <label>Limit the events retrieval to the specified time range</label>
+      <default>true</default>
+    </entry>
+    <entry name="syncRangeStartNumber" type="String">
+      <label>Period for which to retrieve events, quantity</label>
+      <default>3</default>
+    </entry>
+    <entry name="syncRangeStartType" type="String">
+      <label>Period for which to retrieve events, type</label>
+      <default>M</default>
+    </entry>
+    <entry name="collectionsUrlsMappings" type="String">
+      <label>String representation of the mappings between discovered collections and configured URLs</label>
+      <default></default>
+    </entry>
+    <entry name="readOnly" type="Bool">
+      <label>Do not change the actual backend data.</label>
+      <default>false</default>
+    </entry>
+    <entry name="AccountId" type="UInt">
+        <label>The account id in WebAccounts framework.</label>
+    </entry>
+    <entry name="AccountServices" type="StringList">
+        <label>Account enabled services</label>
+        <default></default>
+    </entry>
+  </group>
+</kcfg>
diff --git a/resources/dav/resource/davprotocolattribute.cpp b/resources/dav/resource/davprotocolattribute.cpp
new file mode 100644 (file)
index 0000000..aa4d0e8
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+    Copyright (c) 2009 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "davprotocolattribute.h"
+
+DavProtocolAttribute::DavProtocolAttribute(int protocol)
+    : mDavProtocol(protocol)
+{
+}
+
+int DavProtocolAttribute::davProtocol() const
+{
+    return mDavProtocol;
+}
+
+void DavProtocolAttribute::setDavProtocol(int protocol)
+{
+    mDavProtocol = protocol;
+}
+
+Akonadi::Attribute *DavProtocolAttribute::clone() const
+{
+    return new DavProtocolAttribute(mDavProtocol);
+}
+
+QByteArray DavProtocolAttribute::type() const
+{
+    static const QByteArray sType("davprotocol");
+    return sType;
+}
+
+QByteArray DavProtocolAttribute::serialized() const
+{
+    return QByteArray::number(mDavProtocol);
+}
+
+void DavProtocolAttribute::deserialize(const QByteArray &data)
+{
+    mDavProtocol = data.toInt();
+}
diff --git a/resources/dav/resource/davprotocolattribute.h b/resources/dav/resource/davprotocolattribute.h
new file mode 100644 (file)
index 0000000..7761b14
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+    Copyright (c) 2009 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DAVPROTOCOLATTRIBUTE_H
+#define DAVPROTOCOLATTRIBUTE_H
+
+#include <attribute.h>
+
+#include <QtCore/QString>
+
+class DavProtocolAttribute : public Akonadi::Attribute
+{
+public:
+    explicit DavProtocolAttribute(int protocol = 0);
+
+    void setDavProtocol(int protocol);
+    int davProtocol() const;
+
+    Akonadi::Attribute *clone() const Q_DECL_OVERRIDE;
+    QByteArray type() const Q_DECL_OVERRIDE;
+    QByteArray serialized() const Q_DECL_OVERRIDE;
+    void deserialize(const QByteArray &data) Q_DECL_OVERRIDE;
+
+private:
+    int mDavProtocol;
+};
+
+#endif
diff --git a/resources/dav/resource/searchdialog.cpp b/resources/dav/resource/searchdialog.cpp
new file mode 100644 (file)
index 0000000..54fd9e8
--- /dev/null
@@ -0,0 +1,210 @@
+/*
+ *  Copyright (c) 2011 Grégory Oestreicher <greg@kamago.net>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "searchdialog.h"
+
+#include "davcollectionsfetchjob.h"
+#include "davmanager.h"
+#include "davprincipalsearchjob.h"
+#include "davprotocolbase.h"
+#include "davutils.h"
+
+#include "davresource_debug.h"
+#include <QIcon>
+#include <KMessageBox>
+#include <KLocalizedString>
+
+#include <QtCore/QUrl>
+#include <QStandardItemModel>
+#include <QVBoxLayout>
+#include <QDialogButtonBox>
+
+SearchDialog::SearchDialog(QWidget *parent)
+    : QDialog(parent), mModel(new QStandardItemModel(this)), mSubJobCount(0)
+{
+    QWidget *mainWidget = new QWidget(this);
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    setLayout(mainLayout);
+    mainLayout->addWidget(mainWidget);
+    mUi.setupUi(mainWidget);
+    mUi.credentialsGroup->setVisible(false);
+    mUi.searchResults->setModel(mModel);
+
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
+    okButton->setDefault(true);
+    okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
+    connect(buttonBox, &QDialogButtonBox::accepted, this, &SearchDialog::accept);
+    connect(buttonBox, &QDialogButtonBox::rejected, this, &SearchDialog::reject);
+    mainLayout->addWidget(buttonBox);
+    buttonBox->button(QDialogButtonBox::Ok)->setText(i18n("Add Selected Items"));
+
+    connect(mUi.searchUrl, &KLineEdit::textChanged, this, &SearchDialog::checkUserInput);
+    connect(mUi.searchParam, &KLineEdit::textChanged, this, &SearchDialog::checkUserInput);
+    connect(mUi.searchButton, &QPushButton::clicked, this, &SearchDialog::search);
+
+    checkUserInput();
+}
+
+SearchDialog::~SearchDialog()
+{
+}
+
+bool SearchDialog::useDefaultCredentials() const
+{
+    return mUi.useDefaultCreds->isChecked();
+}
+
+void SearchDialog::setUsername(const QString &user)
+{
+    mUi.username->setText(user);
+}
+
+QString SearchDialog::username() const
+{
+    return mUi.username->text();
+}
+
+void SearchDialog::setPassword(const QString &password)
+{
+    mUi.password->setText(password);
+}
+
+QString SearchDialog::password() const
+{
+    return mUi.password->text();
+}
+
+QStringList SearchDialog::selection() const
+{
+    QModelIndexList indexes = mUi.searchResults->selectionModel()->selectedIndexes();
+    QStringList ret;
+    ret.reserve(indexes.count());
+    foreach (const QModelIndex &index, indexes) {
+        qCritical() << "SELECTED DATA: " << index.data(Qt::UserRole + 1).toString();
+        ret << index.data(Qt::UserRole + 1).toString();
+    }
+    return ret;
+}
+
+void SearchDialog::checkUserInput()
+{
+    if (mUi.searchUrl->text().isEmpty() || mUi.searchParam->text().isEmpty()) {
+        mUi.searchButton->setEnabled(false);
+    } else {
+        mUi.searchButton->setEnabled(true);
+    }
+}
+
+void SearchDialog::search()
+{
+    mUi.searchResults->setEnabled(false);
+    mModel->clear();
+    DavPrincipalSearchJob::FilterType filter;
+
+    if (mUi.searchType->currentIndex() == 0) {
+        filter = DavPrincipalSearchJob::DisplayName;
+    } else {
+        filter = DavPrincipalSearchJob::EmailAddress;
+    }
+
+    QUrl url(mUi.searchUrl->text());
+    url.setUserInfo(QString());
+    DavUtils::DavUrl davUrl;
+    davUrl.setUrl(url);
+
+    DavPrincipalSearchJob *job = new DavPrincipalSearchJob(davUrl, filter, mUi.searchParam->text());
+
+    const DavProtocolBase *proto = DavManager::self()->davProtocol(DavUtils::CalDav);
+    job->fetchProperty(proto->principalHomeSet(), proto->principalHomeSetNS());
+
+    proto = DavManager::self()->davProtocol(DavUtils::CardDav);
+    job->fetchProperty(proto->principalHomeSet(), proto->principalHomeSetNS());
+
+    connect(job, &DavPrincipalSearchJob::result, this, &SearchDialog::onSearchJobFinished);
+    job->start();
+}
+
+void SearchDialog::onSearchJobFinished(KJob *job)
+{
+    if (job->error()) {
+        KMessageBox::error(this, i18n("An error occurred when executing search:\n%1", job->errorText()));
+        return;
+    }
+
+    DavPrincipalSearchJob *davJob = qobject_cast<DavPrincipalSearchJob *>(job);
+    QList<DavPrincipalSearchJob::Result> results = davJob->results();
+
+    const DavProtocolBase *caldav = DavManager::self()->davProtocol(DavUtils::CalDav);
+    DavUtils::DavUrl davUrl = davJob->davUrl();
+    QUrl url = davUrl.url();
+
+    foreach (const DavPrincipalSearchJob::Result &result, results) {
+        if (result.value.startsWith(QLatin1Char('/'))) {
+            url.setPath(result.value, QUrl::TolerantMode);
+        } else {
+            QUrl tmp(result.value);
+            tmp.setUserInfo(QString());
+            url = tmp;
+        }
+        davUrl.setUrl(url);
+
+        if (result.property == caldav->principalHomeSet()) {
+            davUrl.setProtocol(DavUtils::CalDav);
+        } else {
+            davUrl.setProtocol(DavUtils::CardDav);
+        }
+
+        DavCollectionsFetchJob *fetchJob = new DavCollectionsFetchJob(davUrl);
+        connect(fetchJob, &DavCollectionsFetchJob::result, this, &SearchDialog::onCollectionsFetchJobFinished);
+        fetchJob->start();
+        ++mSubJobCount;
+    }
+}
+
+void SearchDialog::onCollectionsFetchJobFinished(KJob *job)
+{
+    --mSubJobCount;
+
+    if (mSubJobCount == 0) {
+        mUi.searchResults->setEnabled(true);
+    }
+
+    if (job->error()) {
+        if (mSubJobCount == 0) {
+            KMessageBox::error(this, i18n("An error occurred when executing search:\n%1", job->errorText()));
+        }
+        return;
+    }
+
+    DavCollectionsFetchJob *davJob = qobject_cast<DavCollectionsFetchJob *>(job);
+    DavCollection::List collections = davJob->collections();
+
+    foreach (const DavCollection &collection, collections) {
+        QStandardItem *item = new QStandardItem(collection.displayName());
+        QString data(DavUtils::protocolName(collection.protocol()) + QLatin1Char('|') + collection.url());
+        item->setData(data, Qt::UserRole + 1);
+        item->setToolTip(collection.url());
+        if (collection.protocol() == DavUtils::CalDav) {
+            item->setIcon(QIcon::fromTheme(QStringLiteral("view-calendar")));
+        } else {
+            item->setIcon(QIcon::fromTheme(QStringLiteral("view-pim-contacts")));
+        }
+        mModel->appendRow(item);
+    }
+}
diff --git a/resources/dav/resource/searchdialog.h b/resources/dav/resource/searchdialog.h
new file mode 100644 (file)
index 0000000..581ba4a
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ *  Copyright (c) 2011 Grégory Oestreicher <greg@kamago.net>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef SEARCHDIALOG_H
+#define SEARCHDIALOG_H
+
+#include "ui_searchdialog.h"
+
+#include <QDialog>
+
+class KJob;
+class QStandardItemModel;
+
+class SearchDialog : public QDialog
+{
+    Q_OBJECT
+
+public:
+    explicit SearchDialog(QWidget *parent = Q_NULLPTR);
+    virtual ~SearchDialog();
+
+    bool useDefaultCredentials() const;
+
+    void setUsername(const QString &user);
+    QString username() const;
+
+    void setPassword(const QString &password);
+    QString password() const;
+
+    QStringList selection() const;
+
+private Q_SLOTS:
+    void checkUserInput();
+    void search();
+    void onSearchJobFinished(KJob *job);
+    void onCollectionsFetchJobFinished(KJob *job);
+
+private:
+    Ui::SearchDialog mUi;
+    QStandardItemModel *mModel;
+    int mSubJobCount;
+};
+
+#endif // SEARCHDIALOG_H
diff --git a/resources/dav/resource/searchdialog.ui b/resources/dav/resource/searchdialog.ui
new file mode 100644 (file)
index 0000000..f101297
--- /dev/null
@@ -0,0 +1,221 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SearchDialog</class>
+ <widget class="QWidget" name="SearchDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>557</width>
+    <height>670</height>
+   </rect>
+  </property>
+  <property name="locale">
+   <locale language="English" country="UnitedStates"/>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QRadioButton" name="useDefaultCreds">
+     <property name="text">
+      <string>Use global credentials</string>
+     </property>
+     <property name="checked">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QRadioButton" name="useSpecificCreds">
+     <property name="text">
+      <string>Use specific credentials</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="credentialsGroup">
+     <property name="title">
+      <string>Credentials</string>
+     </property>
+     <layout class="QFormLayout" name="formLayout_2">
+      <item row="0" column="0">
+       <widget class="QLabel" name="label_4">
+        <property name="toolTip">
+         <string comment="The user's login on the remote server"/>
+        </property>
+        <property name="locale">
+         <locale language="English" country="UnitedStates"/>
+        </property>
+        <property name="text">
+         <string>Username</string>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="1">
+       <widget class="KLineEdit" name="username"/>
+      </item>
+      <item row="1" column="0">
+       <widget class="QLabel" name="label_5">
+        <property name="toolTip">
+         <string comment="The user's login on the remote server"/>
+        </property>
+        <property name="locale">
+         <locale language="English" country="UnitedStates"/>
+        </property>
+        <property name="text">
+         <string>Password</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="1">
+       <widget class="KLineEdit" name="password">
+        <property name="echoMode">
+         <enum>QLineEdit::Password</enum>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <layout class="QFormLayout" name="formLayout">
+     <item row="0" column="0">
+      <widget class="QLabel" name="label">
+       <property name="text">
+        <string>Search URL</string>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="1">
+      <widget class="KLineEdit" name="searchUrl"/>
+     </item>
+     <item row="1" column="0">
+      <widget class="QLabel" name="label_2">
+       <property name="text">
+        <string>Search for</string>
+       </property>
+      </widget>
+     </item>
+     <item row="1" column="1">
+      <layout class="QHBoxLayout" name="horizontalLayout">
+       <item>
+        <widget class="QComboBox" name="searchType">
+         <item>
+          <property name="text">
+           <string>a person named</string>
+          </property>
+         </item>
+         <item>
+          <property name="text">
+           <string>a contact with email</string>
+          </property>
+         </item>
+        </widget>
+       </item>
+       <item>
+        <widget class="KLineEdit" name="searchParam"/>
+       </item>
+      </layout>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_2">
+     <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QPushButton" name="searchButton">
+       <property name="text">
+        <string>Search</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer_2">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QListView" name="searchResults">
+     <property name="editTriggers">
+      <set>QAbstractItemView::NoEditTriggers</set>
+     </property>
+     <property name="selectionMode">
+      <enum>QAbstractItemView::ExtendedSelection</enum>
+     </property>
+     <property name="iconSize">
+      <size>
+       <width>24</width>
+       <height>24</height>
+      </size>
+     </property>
+     <property name="spacing">
+      <number>2</number>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KLineEdit</class>
+   <extends>QLineEdit</extends>
+   <header>klineedit.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>searchParam</sender>
+   <signal>returnPressed()</signal>
+   <receiver>searchButton</receiver>
+   <slot>animateClick()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>358</x>
+     <y>212</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>107</x>
+     <y>243</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>useSpecificCreds</sender>
+   <signal>toggled(bool)</signal>
+   <receiver>credentialsGroup</receiver>
+   <slot>setVisible(bool)</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>82</x>
+     <y>43</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>100</x>
+     <y>68</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
diff --git a/resources/dav/resource/settings.cpp b/resources/dav/resource/settings.cpp
new file mode 100644 (file)
index 0000000..2dbb036
--- /dev/null
@@ -0,0 +1,706 @@
+/*
+    Copyright (c) 2009 Grégory Oestreicher <greg@kamago.net>
+      Based on an original work for the IMAP resource which is :
+    Copyright (c) 2008 Volker Krause <vkrause@kde.org>
+    Copyright (c) 2008 Omat Holding B.V. <info@omat.nl>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "settings.h"
+
+#include "settingsadaptor.h"
+#ifdef HAVE_ACCOUNTS
+#include "../../shared/singlefileresource/getcredentialsjob.h"
+#endif
+
+#include <kapplication.h>
+#include "davresource_debug.h"
+#include <klineedit.h>
+#include <KLocalizedString>
+
+#include <kwallet.h>
+
+#include <QtCore/QByteArray>
+#include <QtCore/QDataStream>
+#include <QtCore/QFile>
+#include <QtCore/QFileInfo>
+#include <QtCore/QPointer>
+#include <QtCore/QRegExp>
+#include <QtCore/QUrl>
+#include <QtDBus/QDBusConnection>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QVBoxLayout>
+#include <QDialogButtonBox>
+#include <QDialog>
+#include <QPushButton>
+
+#ifdef HAVE_ACCOUNTS
+#include <Accounts/Account>
+#include <Accounts/Manager>
+#include <QStandardPaths>
+#endif
+
+class SettingsHelper
+{
+public:
+    SettingsHelper()
+        : q(Q_NULLPTR)
+    {
+    }
+
+    ~SettingsHelper()
+    {
+        delete q;
+    }
+
+    Settings *q;
+};
+
+Q_GLOBAL_STATIC(SettingsHelper, s_globalSettings)
+
+Settings::UrlConfiguration::UrlConfiguration()
+{
+}
+
+Settings::UrlConfiguration::UrlConfiguration(const QString &serialized)
+{
+    const QStringList splitString = serialized.split(QLatin1Char('|'));
+
+    if (splitString.size() == 3) {
+        mUrl = splitString.at(2);
+        mProtocol = DavUtils::protocolByName(splitString.at(1));
+        mUser = splitString.at(0);
+    }
+}
+
+QString Settings::UrlConfiguration::serialize()
+{
+    QString serialized = mUser;
+    serialized.append(QLatin1Char('|')).append(DavUtils::protocolName(DavUtils::Protocol(mProtocol)));
+    serialized.append(QLatin1Char('|')).append(mUrl);
+    return serialized;
+}
+
+Settings *Settings::self()
+{
+    if (!s_globalSettings->q) {
+        new Settings;
+        s_globalSettings->q->load();
+    }
+
+    return s_globalSettings->q;
+}
+
+Settings::Settings()
+    : SettingsBase(), mWinId(0)
+{
+
+#ifdef HAVE_ACCOUNTS
+    m_manager = 0;
+#endif
+    Q_ASSERT(!s_globalSettings->q);
+    s_globalSettings->q = this;
+
+    new SettingsAdaptor(this);
+    QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"), this,
+            QDBusConnection::ExportAdaptors | QDBusConnection::ExportScriptableContents);
+
+    if (settingsVersion() == 1) {
+        updateToV2();
+    }
+
+    if (settingsVersion() == 2) {
+        updateToV3();
+    }
+}
+
+Settings::~Settings()
+{
+    QMapIterator<QString, UrlConfiguration *> it(mUrls);
+    while (it.hasNext()) {
+        it.next();
+        delete it.value();
+    }
+}
+
+void Settings::setWinId(WId winId)
+{
+    mWinId = winId;
+}
+
+void Settings::cleanup()
+{
+    KWallet::Wallet *wallet = KWallet::Wallet::openWallet(KWallet::Wallet::NetworkWallet(), mWinId);
+    if (wallet && wallet->isOpen()) {
+        if (wallet->hasFolder(KWallet::Wallet::PasswordFolder())) {
+            wallet->setFolder(KWallet::Wallet::PasswordFolder());
+            const QString entry = mResourceIdentifier + QLatin1Char(',') + QStringLiteral("$default$");
+            wallet->removeEntry(entry);
+        }
+        delete wallet;
+    }
+    QFile cacheFile(mCollectionsUrlsMappingCache);
+    cacheFile.remove();
+}
+
+void Settings::setResourceIdentifier(const QString &identifier)
+{
+    mResourceIdentifier = identifier;
+}
+
+void Settings::setDefaultPassword(const QString &password)
+{
+    savePassword(mResourceIdentifier, QStringLiteral("$default$"), password);
+}
+
+QString Settings::defaultPassword()
+{
+    return loadPassword(mResourceIdentifier, QStringLiteral("$default$"));
+}
+
+DavUtils::DavUrl::List Settings::configuredDavUrls()
+{
+    if (mUrls.isEmpty()) {
+        buildUrlsList();
+    }
+    DavUtils::DavUrl::List davUrls;
+    davUrls.reserve(mUrls.count());
+    QMapIterator<QString, UrlConfiguration *> it(mUrls);
+
+    while (it.hasNext()) {
+        it.next();
+        QStringList split = it.key().split(QLatin1Char(','));
+        davUrls << configuredDavUrl(DavUtils::protocolByName(split.at(1)), split.at(0));
+    }
+
+    return davUrls;
+}
+
+DavUtils::DavUrl Settings::configuredDavUrl(DavUtils::Protocol proto, const QString &searchUrl, const QString &finalUrl)
+{
+    if (mUrls.isEmpty()) {
+        buildUrlsList();
+    }
+
+    QUrl fullUrl;
+
+    if (!finalUrl.isEmpty()) {
+        fullUrl = QUrl::fromUserInput(finalUrl);
+    } else {
+        fullUrl = QUrl::fromUserInput(searchUrl);
+    }
+
+    const QString user = username(proto, searchUrl);
+    fullUrl.setUserName(user);
+    fullUrl.setPassword(password(proto, searchUrl));
+
+    return DavUtils::DavUrl(fullUrl, proto);
+}
+
+DavUtils::DavUrl Settings::davUrlFromCollectionUrl(const QString &collectionUrl, const QString &finalUrl)
+{
+    if (mCollectionsUrlsMapping.isEmpty()) {
+        loadMappings();
+    }
+
+    DavUtils::DavUrl davUrl;
+    QString targetUrl = finalUrl.isEmpty() ? collectionUrl : finalUrl;
+
+    if (mCollectionsUrlsMapping.contains(collectionUrl)) {
+        QStringList split = mCollectionsUrlsMapping[ collectionUrl ].split(QLatin1Char(','));
+        if (split.size() == 2) {
+            davUrl = configuredDavUrl(DavUtils::protocolByName(split.at(1)), split.at(0), targetUrl);
+        }
+    }
+
+    return davUrl;
+}
+
+void Settings::addCollectionUrlMapping(DavUtils::Protocol proto, const QString &collectionUrl, const QString &configuredUrl)
+{
+    if (mCollectionsUrlsMapping.isEmpty()) {
+        loadMappings();
+    }
+
+    QString value = configuredUrl + QLatin1Char(',') + DavUtils::protocolName(proto);
+    mCollectionsUrlsMapping.insert(collectionUrl, value);
+
+    // Update the cache now
+    //QMap<QString, QString> tmp( mCollectionsUrlsMapping );
+    QFileInfo cacheFileInfo = QFileInfo(mCollectionsUrlsMappingCache);
+    if (!cacheFileInfo.dir().exists()) {
+        QDir::root().mkpath(cacheFileInfo.dir().absolutePath());
+    }
+
+    QFile cacheFile(mCollectionsUrlsMappingCache);
+    if (cacheFile.open(QIODevice::WriteOnly)) {
+        QDataStream cache(&cacheFile);
+        cache.setVersion(QDataStream::Qt_4_7);
+        cache << mCollectionsUrlsMapping;
+        cacheFile.close();
+    }
+}
+
+QStringList Settings::mappedCollections(DavUtils::Protocol proto, const QString &configuredUrl)
+{
+    if (mCollectionsUrlsMapping.isEmpty()) {
+        loadMappings();
+    }
+
+    QString value = configuredUrl + QLatin1Char(',') + DavUtils::protocolName(proto);
+    return mCollectionsUrlsMapping.keys(value);
+}
+
+void Settings::reloadConfig()
+{
+#ifdef HAVE_ACCOUNTS
+    importFromAccounts();
+#endif
+    buildUrlsList();
+    updateRemoteUrls();
+    loadMappings();
+}
+
+void Settings::newUrlConfiguration(Settings::UrlConfiguration *urlConfig)
+{
+    QString key = urlConfig->mUrl + QLatin1Char(',') + DavUtils::protocolName(DavUtils::Protocol(urlConfig->mProtocol));
+
+    if (mUrls.contains(key)) {
+        removeUrlConfiguration(DavUtils::Protocol(urlConfig->mProtocol), urlConfig->mUrl);
+    }
+
+    mUrls[ key ] = urlConfig;
+    if (urlConfig->mUser != QStringLiteral("$default$")) {
+        savePassword(key, urlConfig->mUser, urlConfig->mPassword);
+    }
+    updateRemoteUrls();
+}
+
+void Settings::removeUrlConfiguration(DavUtils::Protocol proto, const QString &url)
+{
+    QString key = url + QLatin1Char(',') + DavUtils::protocolName(proto);
+
+    if (!mUrls.contains(key)) {
+        return;
+    }
+
+    delete mUrls[ key ];
+    mUrls.remove(key);
+    updateRemoteUrls();
+}
+
+Settings::UrlConfiguration *Settings::urlConfiguration(DavUtils::Protocol proto, const QString &url)
+{
+    QString key = url + QLatin1Char(',') + DavUtils::protocolName(proto);
+
+    UrlConfiguration *ret = Q_NULLPTR;
+    if (mUrls.contains(key)) {
+        ret = mUrls[ key ];
+    }
+
+    return ret;
+}
+
+// DavUtils::Protocol Settings::protocol( const QString &url ) const
+// {
+//   if ( mUrls.contains( url ) )
+//     return DavUtils::Protocol( mUrls[ url ]->mProtocol );
+//   else
+//     return DavUtils::CalDav;
+// }
+
+QString Settings::username(DavUtils::Protocol proto, const QString &url) const
+{
+    QString key = url + QLatin1Char(',') + DavUtils::protocolName(proto);
+
+    if (mUrls.contains(key))
+        if (mUrls[ key ]->mUser == QLatin1String("$default$")) {
+            return defaultUsername();
+        }
+#ifdef HAVE_ACCOUNTS
+        else if (mUrls[ key ]->mUser == QLatin1String("$accounts$")) {
+            return accountsUsername();
+        }
+#endif
+        else {
+            return mUrls[ key ]->mUser;
+        }
+    else {
+        return QString();
+    }
+}
+
+QString Settings::password(DavUtils::Protocol proto, const QString &url)
+{
+    QString key = url + QLatin1Char(',') + DavUtils::protocolName(proto);
+
+    if (mUrls.contains(key))
+        if (mUrls[ key ]->mUser == QLatin1String("$default$")) {
+            return defaultPassword();
+        } else {
+            return mUrls[ key ]->mPassword;
+        }
+    else {
+        return QString();
+    }
+}
+
+QDateTime Settings::getSyncRangeStart() const
+{
+    QDateTime start = QDateTime::currentDateTimeUtc();
+    start.setTime(QTime());
+    const int delta = - syncRangeStartNumber().toUInt();
+
+    if (syncRangeStartType() == QLatin1String("D")) {
+        start = start.addDays(delta);
+    } else if (syncRangeStartType() == QLatin1String("M")) {
+        start = start.addMonths(delta);
+    } else if (syncRangeStartType() == QLatin1String("Y")) {
+        start = start.addYears(delta);
+    } else {
+        start = QDateTime();
+    }
+
+    return start;
+}
+
+#ifdef HAVE_ACCOUNTS
+void Settings::importFromAccounts()
+{
+    qCDebug(DAVRESOURCE_LOG);
+    Accounts::AccountId id = accountId();
+    qCDebug(DAVRESOURCE_LOG) << "Account Id: " << id;
+
+    if (!m_manager) {
+        m_manager = new Accounts::Manager(this);
+    }
+
+    if (!m_manager->accountList().contains(id)) {
+        return;
+    }
+
+    removeAccountsDisabledServices();
+    addAccountsEnabledServices();
+
+    setSettingsVersion(3);
+    writeConfig();
+}
+
+void Settings::addAccountsEnabledServices()
+{
+    qCDebug(DAVRESOURCE_LOG);
+    Accounts::Account *acc = m_manager->account(accountId());
+    QStringList enabledServices = accountServices();
+    qCDebug(DAVRESOURCE_LOG) << "Enabled" << enabledServices;
+    foreach (QString serviceType, enabledServices) {
+        Accounts::ServiceList services = acc->services(serviceType);
+        foreach (const Accounts::Service &service, services) {
+            configureAccountService(acc, service);
+        }
+    }
+}
+
+void Settings::removeAccountsDisabledServices()
+{
+    qCDebug(DAVRESOURCE_LOG);
+    QStringList urls = remoteUrls();
+    for (int i = 0; i < urls.size(); ++i) {
+        if (!urls.at(i).startsWith(QStringLiteral("$accounts$"))) {
+            continue;
+        }
+
+        if (urls.at(i).contains(QStringLiteral("carddav"))
+                && accountServices().contains(QStringLiteral("dav-contacts"))) {
+            continue;
+        }
+        if (urls.at(i).contains(QStringLiteral("caldav"))
+                && accountServices().contains(QStringLiteral("dav-calendar"))) {
+            continue;
+        }
+
+        urls.removeAt(i);
+    }
+
+    setRemoteUrls(urls);
+}
+
+void Settings::configureAccountService(Accounts::Account *acc, const Accounts::Service &service)
+{
+    qCDebug(DAVRESOURCE_LOG) << "Configuring service: " << service.name();
+
+    acc->selectService();
+    QString domain = acc->valueAsString(QStringLiteral("dav/scheme")) + QStringLiteral("://") + acc->valueAsString(QStringLiteral("dav/host"));
+    acc->selectService(service);
+
+    QString type;
+    if (service.serviceType() == QLatin1String("dav-contacts")) {
+        type = QStringLiteral("CardDav");
+    } else {
+        type = QStringLiteral("CalDav");
+    }
+
+    QString url = QStringLiteral("$accounts$|") + type + QLatin1Char('|') + domain + acc->valueAsString(QStringLiteral("dav/path"));
+    qCDebug(DAVRESOURCE_LOG) << url;
+    acc->selectService();
+
+    QStringList urls = remoteUrls();
+    foreach (const QString &serializedUrl, urls) {
+        if (url == serializedUrl) {
+            qCDebug(DAVRESOURCE_LOG) << "Url already configured";
+            return;
+        }
+    }
+
+    qCDebug(DAVRESOURCE_LOG) << "Adding url";
+    urls.append(url);
+    setRemoteUrls(urls);
+}
+#endif
+
+void Settings::buildUrlsList()
+{
+    foreach (const QString &serializedUrl, remoteUrls()) {
+        UrlConfiguration *urlConfig = new UrlConfiguration(serializedUrl);
+        QString key = urlConfig->mUrl + QLatin1Char(',') + DavUtils::protocolName(DavUtils::Protocol(urlConfig->mProtocol));
+        QString pass = loadPassword(key, urlConfig->mUser);
+        if (!pass.isNull()) {
+            urlConfig->mPassword = pass;
+            mUrls[ key ] = urlConfig;
+        } else {
+            delete urlConfig;
+        }
+    }
+}
+
+void Settings::loadMappings()
+{
+    QString collectionsMappingCacheBase = QStringLiteral("akonadi-davgroupware/%1_c2u.dat").arg(KApplication::applicationName());
+    mCollectionsUrlsMappingCache = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + collectionsMappingCacheBase;
+    QFile collectionsMappingsCache(mCollectionsUrlsMappingCache);
+
+    if (collectionsMappingsCache.exists()) {
+        if (collectionsMappingsCache.open(QIODevice::ReadOnly)) {
+            QDataStream cache(&collectionsMappingsCache);
+            cache >> mCollectionsUrlsMapping;
+            collectionsMappingsCache.close();
+        }
+    } else if (!collectionsUrlsMappings().isEmpty()) {
+        QByteArray rawMappings = QByteArray::fromBase64(collectionsUrlsMappings().toLatin1());
+        QDataStream stream(&rawMappings, QIODevice::ReadOnly);
+        stream >> mCollectionsUrlsMapping;
+        setCollectionsUrlsMappings(QString());
+    }
+}
+
+void Settings::updateRemoteUrls()
+{
+    QStringList newUrls;
+    newUrls.reserve(mUrls.count());
+
+    QMapIterator<QString, UrlConfiguration *> it(mUrls);
+    while (it.hasNext()) {
+        it.next();
+        newUrls << it.value()->serialize();
+    }
+
+    setRemoteUrls(newUrls);
+}
+
+void Settings::savePassword(const QString &key, const QString &user, const QString &password)
+{
+    QString entry = key + QLatin1Char(',') + user;
+    mPasswordsCache[entry] = password;
+
+    KWallet::Wallet *wallet = KWallet::Wallet::openWallet(KWallet::Wallet::NetworkWallet(), mWinId);
+    if (!wallet) {
+        return;
+    }
+
+    if (!wallet->hasFolder(KWallet::Wallet::PasswordFolder())) {
+        wallet->createFolder(KWallet::Wallet::PasswordFolder());
+    }
+
+    if (!wallet->setFolder(KWallet::Wallet::PasswordFolder())) {
+        return;
+    }
+
+    wallet->writePassword(entry, password);
+}
+
+QString Settings::loadPassword(const QString &key, const QString &user)
+{
+    QString entry;
+    QString pass;
+
+    if (user == QLatin1String("$default$")) {
+        entry = mResourceIdentifier + QLatin1Char(',') + user;
+    }
+#ifdef HAVE_ACCOUNTS
+    else if (user == QLatin1String("$accounts$")) {
+        return loadPasswordFromAccounts();
+    }
+#endif
+    else {
+        entry = key + QLatin1Char(',') + user;
+    }
+
+    if (mPasswordsCache.contains(entry)) {
+        return mPasswordsCache[entry];
+    }
+
+    KWallet::Wallet *wallet = KWallet::Wallet::openWallet(KWallet::Wallet::NetworkWallet(), mWinId);
+    if (wallet) {
+        if (!wallet->hasFolder(KWallet::Wallet::PasswordFolder())) {
+            wallet->createFolder(KWallet::Wallet::PasswordFolder());
+        }
+
+        if (wallet->setFolder(KWallet::Wallet::PasswordFolder())) {
+            if (!wallet->hasEntry(entry)) {
+                pass = promptForPassword(user);
+                wallet->writePassword(entry, pass);
+            } else {
+                wallet->readPassword(entry, pass);
+            }
+        }
+    }
+
+    if (pass.isNull() && !KWallet::Wallet::isEnabled()) {
+        pass = promptForPassword(user);
+    }
+
+    if (!pass.isNull()) {
+        mPasswordsCache[entry] = pass;
+    }
+
+    return pass;
+}
+
+#ifdef HAVE_ACCOUNTS
+QString Settings::loadPasswordFromAccounts()
+{
+    qCDebug(DAVRESOURCE_LOG) << "Getting credentials for: " << accountId();
+    GetCredentialsJob *job = new GetCredentialsJob(accountId());
+    job->exec();
+
+    return job->credentialsData().value(QLatin1String("Secret")).toString();
+}
+
+QString Settings::accountsUsername() const
+{
+    qCDebug(DAVRESOURCE_LOG) << "Getting credentials for: " << accountId();
+    GetCredentialsJob *job = new GetCredentialsJob(accountId());
+    job->exec();
+
+    qCDebug(DAVRESOURCE_LOG) << "Got some: " << job->credentialsData();
+
+    return job->credentialsData().value(QLatin1String("UserName")).toString();
+}
+#endif
+
+QString Settings::promptForPassword(const QString &user)
+{
+    QPointer<QDialog> dlg = new QDialog();
+    QString password;
+
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Help);
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    dlg->setLayout(mainLayout);
+    QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
+    okButton->setDefault(true);
+    okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
+    connect(buttonBox, &QDialogButtonBox::accepted, dlg.data(), &QDialog::accept);
+    connect(buttonBox, &QDialogButtonBox::rejected, dlg.data(), &QDialog::reject);
+
+    QWidget *mainWidget = new QWidget(dlg);
+    mainLayout->addWidget(mainWidget);
+    mainLayout->addWidget(buttonBox);
+    QVBoxLayout *vLayout = new QVBoxLayout();
+    mainWidget->setLayout(vLayout);
+    QLabel *label = new QLabel(i18n("A password is required for user %1",
+                                    (user == QLatin1String("$default$") ? defaultUsername() : user)),
+                               mainWidget
+                              );
+    vLayout->addWidget(label);
+    QHBoxLayout *hLayout = new QHBoxLayout();
+    label = new QLabel(i18n("Password: "), mainWidget);
+    hLayout->addWidget(label);
+    KLineEdit *lineEdit = new KLineEdit();
+    lineEdit->setPasswordMode(true);
+    hLayout->addWidget(lineEdit);
+    vLayout->addLayout(hLayout);
+    lineEdit->setFocus();
+
+    const int result = dlg->exec();
+
+    if (result == QDialog::Accepted && !dlg.isNull()) {
+        password = lineEdit->text();
+    }
+
+    delete dlg;
+    return password;
+}
+
+void Settings::updateToV2()
+{
+    // Take the first URL that was configured to get the username that
+    // has the most chances being the default
+
+    QStringList urls = remoteUrls();
+    if (urls.isEmpty()) {
+        return;
+    }
+
+    QString urlConfigStr = urls.at(0);
+    UrlConfiguration urlConfig(urlConfigStr);
+    QRegExp regexp(QLatin1Char('^') + urlConfig.mUser);
+
+    QMutableStringListIterator it(urls);
+    while (it.hasNext()) {
+        it.next();
+        it.value().replace(regexp, QStringLiteral("$default$"));
+    }
+
+    setDefaultUsername(urlConfig.mUser);
+    QString key = urlConfig.mUrl + QLatin1Char(',') + DavUtils::protocolName(DavUtils::Protocol(urlConfig.mProtocol));
+    QString pass = loadPassword(key, urlConfig.mUser);
+    if (!pass.isNull()) {
+        setDefaultPassword(pass);
+    }
+    setRemoteUrls(urls);
+    setSettingsVersion(2);
+    save();
+}
+
+void Settings::updateToV3()
+{
+    QStringList updatedUrls;
+
+    foreach (const QString &url, remoteUrls()) {
+        QStringList splitUrl = url.split(QLatin1Char('|'));
+
+        if (splitUrl.size() == 3) {
+            DavUtils::Protocol protocol = DavUtils::protocolByTranslatedName(splitUrl.at(1));
+            splitUrl[1] = DavUtils::protocolName(protocol);
+            updatedUrls << splitUrl.join(QStringLiteral("|"));
+        }
+    }
+
+    setRemoteUrls(updatedUrls);
+    setSettingsVersion(3);
+    save();
+}
+
diff --git a/resources/dav/resource/settings.h b/resources/dav/resource/settings.h
new file mode 100644 (file)
index 0000000..81bbb52
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+    Copyright (c) 2009 Grégory Oestreicher <greg@kamago.net>
+      Based on an original work for the IMAP resource which is :
+    Copyright (c) 2008 Volker Krause <vkrause@kde.org>
+    Copyright (c) 2008 Omat Holding B.V. <info@omat.nl>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef SETTINGS_H
+#define SETTINGS_H
+
+#include "settingsbase.h"
+
+#include "davutils.h"
+
+#include <QtCore/QMap>
+
+#ifdef HAVE_ACCOUNTS
+namespace Accounts
+{
+class Service;
+class Account;
+class Manager;
+};
+#endif
+
+class Settings : public SettingsBase
+{
+    Q_OBJECT
+
+public:
+    class UrlConfiguration
+    {
+    public:
+        UrlConfiguration();
+        explicit UrlConfiguration(const QString &serialized);
+
+        /**
+         * Serializes the object.
+         * The string is the concatenation of the fields separated by a "|" :
+         * user|protocol|url
+         * The "protocol" component is the symbolic name of the protocol,
+         * as returned by DavUtils::protocolName().
+         */
+        QString serialize();
+        QString mUrl;
+        QString mUser;
+        QString mPassword;
+        int mProtocol;
+    };
+
+    Settings();
+    virtual ~Settings();
+    static Settings *self();
+    void setWinId(WId wid);
+    void cleanup();
+
+    void setResourceIdentifier(const QString &identifier);
+    void setDefaultPassword(const QString &password);
+    QString defaultPassword();
+
+    DavUtils::DavUrl::List configuredDavUrls();
+
+    /**
+     * Creates and returns the DavUrl that corresponds to the configuration for searchUrl.
+     * If finalUrl is supplied, then it will be used in the returned object instead of the searchUrl.
+     */
+    DavUtils::DavUrl configuredDavUrl(DavUtils::Protocol protocol, const QString &searchUrl, const QString &finalUrl = QString());
+
+    /**
+     * Creates and return the DavUrl from the configured URL that has a mapping with @p collectionUrl.
+     * If @p finalUrl is supplied it will be used in the returned object, else @p collectionUrl will
+     * be used.
+     * If no configured URL can be found the returned DavUrl will have an empty url().
+     */
+    DavUtils::DavUrl davUrlFromCollectionUrl(const QString &collectionUrl, const QString &finalUrl = QString());
+
+    /**
+     * Add a new mapping between the collection URL, as seen on the backend, and the
+     * URL configured by the user. A mapping here means that the collectionUrl has
+     * been discovered by a DavCollectionsFetchJob on the configuredUrl.
+     */
+    void addCollectionUrlMapping(DavUtils::Protocol protocol, const QString &collectionUrl, const QString &configuredUrl);
+
+    /**
+     * Returns the collections URLs mapped behing @p configuredUrl and @p protocol.
+     */
+    QStringList mappedCollections(DavUtils::Protocol protocol, const QString &configuredUrl);
+
+    /**
+     * Reloads the resource configuration taking into account any new modification
+     *
+     * Whenever the resource configuration is modified it needs to be reload in order
+     * to make the resource use the new config. This slot will call the needed methods
+     * to be sure that any new setting is taken into account.
+     */
+    void reloadConfig();
+
+    void newUrlConfiguration(UrlConfiguration *urlConfig);
+    void removeUrlConfiguration(DavUtils::Protocol protocol, const QString &url);
+    UrlConfiguration *urlConfiguration(DavUtils::Protocol protocol, const QString &url);
+
+    //DavUtils::Protocol protocol( const QString &url ) const;
+    QString username(DavUtils::Protocol protocol, const QString &url) const;
+    QString password(DavUtils::Protocol protocol, const QString &url);
+    QDateTime getSyncRangeStart() const;
+
+private:
+#ifdef HAVE_ACCOUNTS
+    void addAccountsEnabledServices();
+    void removeAccountsDisabledServices();
+    void configureAccountService(Accounts::Account *acc, const Accounts::Service &service);
+    void importFromAccounts();
+    QString loadPasswordFromAccounts();
+    QString accountsUsername() const;
+#endif
+    void buildUrlsList();
+    void loadMappings();
+    void updateRemoteUrls();
+    void savePassword(const QString &key, const QString &user, const QString &password);
+    QString loadPassword(const QString &key, const QString &user);
+    QString promptForPassword(const QString &user);
+
+    void updateToV2();
+    void updateToV3();
+
+    WId mWinId;
+#ifdef HAVE_ACCOUNTS
+    Accounts::Manager *m_manager;
+#endif
+    QString mResourceIdentifier;
+    QMap<QString, UrlConfiguration *> mUrls;
+    QMap<QString, QString> mPasswordsCache;
+    QString mCollectionsUrlsMappingCache;
+    QMap<QString, QString> mCollectionsUrlsMapping;
+    QList<UrlConfiguration *> mToDeleteUrlConfigs;
+};
+
+#endif
diff --git a/resources/dav/resource/settingsbase.kcfgc b/resources/dav/resource/settingsbase.kcfgc
new file mode 100644 (file)
index 0000000..6f57756
--- /dev/null
@@ -0,0 +1,8 @@
+File=davgroupwareresource.kcfg
+ClassName=SettingsBase
+Mutators=true
+ItemAccessors=true
+SetUserTexts=true
+Singleton=false
+#IncludeFiles=
+GlobalEnums=true
diff --git a/resources/dav/resource/setupwizard.cpp b/resources/dav/resource/setupwizard.cpp
new file mode 100644 (file)
index 0000000..5c2d34e
--- /dev/null
@@ -0,0 +1,561 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "setupwizard.h"
+
+#include "davcollectionsmultifetchjob.h"
+
+#include <qicon.h>
+#include <KLocalizedString>
+#include <klineedit.h>
+#include <kservice.h>
+#include <kservicetypetrader.h>
+#include <QTextBrowser>
+
+#include <QUrl>
+#include <QButtonGroup>
+#include <QComboBox>
+#include <QCheckBox>
+#include <QFormLayout>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QPushButton>
+#include <QRadioButton>
+#include <QRegExpValidator>
+
+enum GroupwareServers {
+    Citadel,
+    DAVical,
+    eGroupware,
+    OpenGroupware,
+    ScalableOGo,
+    Scalix,
+    Zarafa,
+    Zimbra
+};
+
+static QString settingsToUrl(const QWizard *wizard, const QString &protocol)
+{
+    QString desktopFilePath = wizard->property("providerDesktopFilePath").toString();
+    if (desktopFilePath.isEmpty()) {
+        return QString();
+    }
+
+    KService::Ptr service = KService::serviceByStorageId(desktopFilePath);
+    if (!service) {
+        return QString();
+    }
+
+    QStringList supportedProtocols = service->property(QStringLiteral("X-DavGroupware-SupportedProtocols")).toStringList();
+    if (!supportedProtocols.contains(protocol)) {
+        return QString();
+    }
+
+    QString pathPattern;
+
+    QString pathPropertyName(QStringLiteral("X-DavGroupware-") + protocol + QStringLiteral("Path"));
+    if (service->property(pathPropertyName).isNull()) {
+        return QString();
+    }
+
+    pathPattern.append(service->property(pathPropertyName).toString() + QLatin1Char('/'));
+
+    QString username = wizard->field(QStringLiteral("credentialsUserName")).toString();
+    QString localPart(username);
+    localPart.remove(QRegExp(QLatin1String("@.*$")));
+    pathPattern.replace(QStringLiteral("$user$"), username);
+    pathPattern.replace(QStringLiteral("$localpart$"), localPart);
+    QString providerName;
+    if (!service->property(QStringLiteral("X-DavGroupware-Provider")).isNull()) {
+        providerName = service->property(QStringLiteral("X-DavGroupware-Provider")).toString();
+    }
+    QString localPath = wizard->field(QStringLiteral("installationPath")).toString();
+    if (!localPath.isEmpty()) {
+        if (providerName == QLatin1String("davical")) {
+            if (!localPath.endsWith(QLatin1Char('/'))) {
+                pathPattern.append(localPath + QLatin1Char('/'));
+            } else {
+                pathPattern.append(localPath);
+            }
+        } else {
+            if (!localPath.startsWith(QLatin1Char('/'))) {
+                pathPattern.prepend(QLatin1Char('/') + localPath);
+            } else {
+                pathPattern.prepend(localPath);
+            }
+        }
+    }
+    QUrl url;
+
+    if (!wizard->property("usePredefinedProvider").isNull()) {
+        if (service->property(QStringLiteral("X-DavGroupware-ProviderUsesSSL")).toBool()) {
+            url.setScheme(QStringLiteral("https"));
+        } else {
+            url.setScheme(QStringLiteral("http"));
+        }
+
+        QString hostPropertyName(QStringLiteral("X-DavGroupware-") + protocol + QStringLiteral("Host"));
+        if (service->property(hostPropertyName).isNull()) {
+            return QString();
+        }
+
+        url.setHost(service->property(hostPropertyName).toString());
+        url.setPath(pathPattern);
+    } else {
+        if (wizard->field(QStringLiteral("connectionUseSecureConnection")).toBool()) {
+            url.setScheme(QStringLiteral("https"));
+        } else {
+            url.setScheme(QStringLiteral("http"));
+        }
+
+        QString host = wizard->field(QStringLiteral("connectionHost")).toString();
+        if (host.isEmpty()) {
+            return QString();
+        }
+        QStringList hostParts = host.split(QLatin1Char(':'));
+        url.setHost(hostParts.at(0));
+        url.setPath(pathPattern);
+
+        if (hostParts.size() == 2) {
+            int port = hostParts.at(1).toInt();
+            if (port) {
+                url.setPort(port);
+            }
+        }
+    }
+    return url.toString();
+}
+
+/*
+ * SetupWizard
+ */
+
+SetupWizard::SetupWizard(QWidget *parent)
+    : QWizard(parent)
+{
+    setWindowTitle(i18n("DAV groupware configuration wizard"));
+    setWindowIcon(QIcon::fromTheme(QStringLiteral("folder-remote")));
+    setPage(W_CredentialsPage, new CredentialsPage);
+    setPage(W_PredefinedProviderPage, new PredefinedProviderPage);
+    setPage(W_ServerTypePage, new ServerTypePage);
+    setPage(W_ConnectionPage, new ConnectionPage);
+    setPage(W_CheckPage, new CheckPage);
+}
+
+QString SetupWizard::displayName() const
+{
+    QString desktopFilePath = property("providerDesktopFilePath").toString();
+    if (desktopFilePath.isEmpty()) {
+        return QString();
+    }
+
+    KService::Ptr service = KService::serviceByStorageId(desktopFilePath);
+    if (!service) {
+        return QString();
+    }
+
+    return service->name();
+}
+
+SetupWizard::Url::List SetupWizard::urls() const
+{
+    Url::List urls;
+
+    QString desktopFilePath = property("providerDesktopFilePath").toString();
+    if (desktopFilePath.isEmpty()) {
+        return urls;
+    }
+
+    KService::Ptr service = KService::serviceByStorageId(desktopFilePath);
+    if (!service) {
+        return urls;
+    }
+
+    QStringList supportedProtocols = service->property(QStringLiteral("X-DavGroupware-SupportedProtocols")).toStringList();
+    foreach (const QString &protocol, supportedProtocols) {
+        Url url;
+
+        if (protocol == QLatin1String("CalDav")) {
+            url.protocol = DavUtils::CalDav;
+        } else if (protocol == QLatin1String("CardDav")) {
+            url.protocol = DavUtils::CardDav;
+        } else if (protocol == QLatin1String("GroupDav")) {
+            url.protocol = DavUtils::GroupDav;
+        } else {
+            return urls;
+        }
+
+        QString urlStr = settingsToUrl(this, protocol);
+
+        if (!urlStr.isEmpty()) {
+            url.url = urlStr;
+            url.userName = QStringLiteral("$default$");
+            urls << url;
+        }
+    }
+
+    return urls;
+}
+
+/*
+ * CredentialsPage
+ */
+
+CredentialsPage::CredentialsPage(QWidget *parent)
+    : QWizardPage(parent)
+{
+    setTitle(i18n("Login Credentials"));
+    setSubTitle(i18n("Enter your credentials to login to the groupware server"));
+
+    QFormLayout *layout = new QFormLayout(this);
+
+    mUserName = new KLineEdit;
+    layout->addRow(i18n("User"), mUserName);
+    registerField(QStringLiteral("credentialsUserName*"), mUserName);
+
+    mPassword = new KLineEdit;
+    mPassword->setPasswordMode(true);
+    layout->addRow(i18n("Password"), mPassword);
+    registerField(QStringLiteral("credentialsPassword*"), mPassword);
+}
+
+int CredentialsPage::nextId() const
+{
+    QString userName = field(QStringLiteral("credentialsUserName")).toString();
+    if (userName.endsWith(QStringLiteral("@yahoo.com"))) {
+        KService::List offers;
+        offers = KServiceTypeTrader::self()->query(QStringLiteral("DavGroupwareProvider"), QStringLiteral("Name == 'Yahoo!'"));
+        if (offers.isEmpty()) {
+            return SetupWizard::W_ServerTypePage;
+        }
+
+        wizard()->setProperty("usePredefinedProvider", true);
+        wizard()->setProperty("predefinedProviderName", offers.at(0)->name());
+        wizard()->setProperty("providerDesktopFilePath", offers.at(0)->entryPath());
+        return SetupWizard::W_PredefinedProviderPage;
+    } else {
+        return SetupWizard::W_ServerTypePage;
+    }
+}
+
+/*
+ * PredefinedProviderPage
+ */
+
+PredefinedProviderPage::PredefinedProviderPage(QWidget *parent)
+    : QWizardPage(parent)
+{
+    setTitle(i18n("Predefined provider found"));
+    setSubTitle(i18n("Select if you want to use the auto-detected provider"));
+
+    QVBoxLayout *layout = new QVBoxLayout(this);
+
+    mLabel = new QLabel;
+    layout->addWidget(mLabel);
+
+    mProviderGroup = new QButtonGroup(this);
+    mProviderGroup->setExclusive(true);
+
+    mUseProvider = new QRadioButton;
+    mProviderGroup->addButton(mUseProvider);
+    mUseProvider->setChecked(true);
+    layout->addWidget(mUseProvider);
+
+    mDontUseProvider = new QRadioButton(i18n("No, choose another server"));
+    mProviderGroup->addButton(mDontUseProvider);
+    layout->addWidget(mDontUseProvider);
+}
+
+void PredefinedProviderPage::initializePage()
+{
+    mLabel->setText(i18n("Based on the email address you used as a login, this wizard\n"
+                         "can configure automatically an account for %1 services.\n"
+                         "Do you wish to do so?", wizard()->property("predefinedProviderName").toString()));
+
+    mUseProvider->setText(i18n("Yes, use %1 as provider", wizard()->property("predefinedProviderName").toString()));
+}
+
+int PredefinedProviderPage::nextId() const
+{
+    if (mUseProvider->isChecked()) {
+        return SetupWizard::W_CheckPage;
+    } else {
+        wizard()->setProperty("usePredefinedProvider", QVariant());
+        wizard()->setProperty("providerDesktopFilePath", QVariant());
+        return SetupWizard::W_ServerTypePage;
+    }
+}
+
+/*
+ * ServerTypePage
+ */
+
+bool compareServiceOffers(QPair<QString, QString> off1, QPair<QString, QString> off2)
+{
+    return off1.first.toLower() < off2.first.toLower();
+}
+
+ServerTypePage::ServerTypePage(QWidget *parent)
+    : QWizardPage(parent)
+{
+    setTitle(i18n("Groupware Server"));
+    setSubTitle(i18n("Select the groupware server the resource shall be configured for"));
+
+    mProvidersCombo = new QComboBox(this);
+    KService::List providers;
+    KServiceTypeTrader *trader = KServiceTypeTrader::self();
+    providers = trader->query(QStringLiteral("DavGroupwareProvider"));
+    QList< QPair<QString, QString> > offers;
+    offers.reserve(providers.count());
+    foreach (const KService::Ptr &provider, providers) {
+        offers.append(QPair<QString, QString>(provider->name(), provider->entryPath()));
+    }
+    qSort(offers.begin(), offers.end(), compareServiceOffers);
+    QListIterator< QPair<QString, QString> > it(offers);
+    while (it.hasNext()) {
+        QPair<QString, QString> p = it.next();
+        mProvidersCombo->addItem(p.first, p.second);
+    }
+    registerField(QStringLiteral("provider"), mProvidersCombo, "currentText");
+
+    QVBoxLayout *layout = new QVBoxLayout(this);
+
+    mServerGroup = new QButtonGroup(this);
+    mServerGroup->setExclusive(true);
+
+    QRadioButton *button;
+
+    QHBoxLayout *hLayout = new QHBoxLayout;
+    button = new QRadioButton(i18n("Use one of those servers:"));
+    registerField(QStringLiteral("templateConfiguration"), button);
+    mServerGroup->addButton(button);
+    mServerGroup->setId(button, 0);
+    button->setChecked(true);
+    hLayout->addWidget(button);
+    hLayout->addWidget(mProvidersCombo);
+    hLayout->addStretch(1);
+    layout->addLayout(hLayout);
+
+    button = new QRadioButton(i18n("Configure the resource manually"));
+    connect(button, &QRadioButton::toggled, this, &ServerTypePage::manualConfigToggled);
+    registerField(QStringLiteral("manualConfiguration"), button);
+    mServerGroup->addButton(button);
+    mServerGroup->setId(button, 1);
+    layout->addWidget(button);
+
+    layout->addStretch(1);
+}
+
+void ServerTypePage::manualConfigToggled(bool state)
+{
+    setFinalPage(state);
+    wizard()->button(QWizard::NextButton)->setEnabled(!state);
+}
+
+bool ServerTypePage::validatePage()
+{
+    QVariant desktopFilePath = mProvidersCombo->itemData(mProvidersCombo->currentIndex());
+    if (desktopFilePath.isNull()) {
+        return false;
+    } else {
+        wizard()->setProperty("providerDesktopFilePath", desktopFilePath);
+        return true;
+    }
+}
+
+/*
+ * ConnectionPage
+ */
+
+ConnectionPage::ConnectionPage(QWidget *parent)
+    : QWizardPage(parent), mPreviewLayout(Q_NULLPTR), mCalDavUrlPreview(Q_NULLPTR), mCardDavUrlPreview(Q_NULLPTR), mGroupDavUrlPreview(Q_NULLPTR)
+{
+    setTitle(i18n("Connection"));
+    setSubTitle(i18n("Enter the connection information for the groupware server"));
+
+    mLayout = new QFormLayout;
+    setLayout(mLayout);
+    QRegExp hostnameRegexp(QStringLiteral("^[a-z0-9][.a-z0-9-]*[a-z0-9](?::[0-9]+)?$"));
+    mHost = new KLineEdit;
+    registerField(QStringLiteral("connectionHost*"), mHost);
+    mHost->setValidator(new QRegExpValidator(hostnameRegexp, this));
+    mLayout->addRow(i18n("Host"), mHost);
+
+    mPath = new KLineEdit;
+    mLayout->addRow(i18n("Installation path"), mPath);
+    registerField(QStringLiteral("installationPath"), mPath);
+
+    mUseSecureConnection = new QCheckBox(i18n("Use secure connection"));
+    mUseSecureConnection->setChecked(true);
+    registerField(QStringLiteral("connectionUseSecureConnection"), mUseSecureConnection);
+    mLayout->addRow(QString(), mUseSecureConnection);
+
+    connect(mHost, &KLineEdit::textChanged, this, &ConnectionPage::urlElementChanged);
+    connect(mPath, &KLineEdit::textChanged, this, &ConnectionPage::urlElementChanged);
+    connect(mUseSecureConnection, &QCheckBox::toggled, this, &ConnectionPage::urlElementChanged);
+}
+
+void ConnectionPage::initializePage()
+{
+    KService::Ptr service = KService::serviceByStorageId(wizard()->property("providerDesktopFilePath").toString());
+    if (!service) {
+        return;
+    }
+
+    QString providerInstallationPath = service->property(QStringLiteral("X-DavGroupware-InstallationPath")).toString();
+    if (!providerInstallationPath.isEmpty()) {
+        mPath->setText(providerInstallationPath);
+    }
+
+    QStringList supportedProtocols = service->property(QStringLiteral("X-DavGroupware-SupportedProtocols")).toStringList();
+
+    mPreviewLayout = new QFormLayout;
+    mLayout->addRow(mPreviewLayout);
+
+    if (supportedProtocols.contains(QStringLiteral("CalDav"))) {
+        mCalDavUrlLabel = new QLabel(i18n("Final URL (CalDav)"));
+        mCalDavUrlPreview = new QLabel;
+        mPreviewLayout->addRow(mCalDavUrlLabel, mCalDavUrlPreview);
+    }
+    if (supportedProtocols.contains(QStringLiteral("CardDav"))) {
+        mCardDavUrlLabel = new QLabel(i18n("Final URL (CardDav)"));
+        mCardDavUrlPreview = new QLabel;
+        mPreviewLayout->addRow(mCardDavUrlLabel, mCardDavUrlPreview);
+    }
+    if (supportedProtocols.contains(QStringLiteral("GroupDav"))) {
+        mGroupDavUrlLabel = new QLabel(i18n("Final URL (GroupDav)"));
+        mGroupDavUrlPreview = new QLabel;
+        mPreviewLayout->addRow(mGroupDavUrlLabel, mGroupDavUrlPreview);
+    }
+}
+
+void ConnectionPage::cleanupPage()
+{
+    delete mPreviewLayout;
+
+    if (mCalDavUrlPreview) {
+        delete mCalDavUrlLabel;
+        delete mCalDavUrlPreview;
+        mCalDavUrlPreview = Q_NULLPTR;
+    }
+
+    if (mCardDavUrlPreview) {
+        delete mCardDavUrlLabel;
+        delete mCardDavUrlPreview;
+        mCardDavUrlPreview = Q_NULLPTR;
+    }
+
+    if (mGroupDavUrlPreview) {
+        delete mGroupDavUrlLabel;
+        delete mGroupDavUrlPreview;
+        mGroupDavUrlPreview = Q_NULLPTR;
+    }
+
+    QWizardPage::cleanupPage();
+}
+
+void ConnectionPage::urlElementChanged()
+{
+    if (mHost->text().isEmpty()) {
+        if (mCalDavUrlPreview) {
+            mCalDavUrlPreview->setText(QStringLiteral("-"));
+        }
+        if (mCardDavUrlPreview) {
+            mCardDavUrlPreview->setText(QStringLiteral("-"));
+        }
+        if (mGroupDavUrlPreview) {
+            mGroupDavUrlPreview->setText(QStringLiteral("-"));
+        }
+    } else {
+        if (mCalDavUrlPreview) {
+            mCalDavUrlPreview->setText(settingsToUrl(this->wizard(), QStringLiteral("CalDav")));
+        }
+        if (mCardDavUrlPreview) {
+            mCardDavUrlPreview->setText(settingsToUrl(this->wizard(), QStringLiteral("CardDav")));
+        }
+        if (mGroupDavUrlPreview) {
+            mGroupDavUrlPreview->setText(settingsToUrl(this->wizard(), QStringLiteral("GroupDav")));
+        }
+    }
+}
+
+/*
+ * CheckPage
+ */
+
+CheckPage::CheckPage(QWidget *parent)
+    : QWizardPage(parent)
+{
+    setTitle(i18n("Test Connection"));
+    setSubTitle(i18n("You can test now whether the groupware server can be accessed with the current configuration"));
+    setFinalPage(true);
+
+    QVBoxLayout *layout = new QVBoxLayout(this);
+
+    QPushButton *button = new QPushButton(i18n("Test Connection"));
+    layout->addWidget(button);
+
+    mStatusLabel = new QTextBrowser;
+    layout->addWidget(mStatusLabel);
+
+    connect(button, &QRadioButton::clicked, this, &CheckPage::checkConnection);
+}
+
+void CheckPage::checkConnection()
+{
+    mStatusLabel->clear();
+
+    DavUtils::DavUrl::List davUrls;
+
+    // convert list of SetupWizard::Url to list of DavUtils::DavUrl
+    const SetupWizard::Url::List urls = static_cast<SetupWizard *>(wizard())->urls();
+    foreach (const SetupWizard::Url &url, urls) {
+        DavUtils::DavUrl davUrl;
+        davUrl.setProtocol(url.protocol);
+
+        QUrl serverUrl(url.url);
+        serverUrl.setUserName(wizard()->field(QStringLiteral("credentialsUserName")).toString());
+        serverUrl.setPassword(wizard()->field(QStringLiteral("credentialsPassword")).toString());
+        davUrl.setUrl(serverUrl);
+
+        davUrls << davUrl;
+    }
+
+    // start the dav collections fetch job to test connectivity
+    DavCollectionsMultiFetchJob *job = new DavCollectionsMultiFetchJob(davUrls, this);
+    connect(job, &DavCollectionsMultiFetchJob::result, this, &CheckPage::onFetchDone);
+    job->start();
+}
+
+void CheckPage::onFetchDone(KJob *job)
+{
+    QString msg;
+    QPixmap icon;
+
+    if (job->error()) {
+        msg = i18n("An error occurred: %1", job->errorText());
+        icon = QIcon::fromTheme(QStringLiteral("dialog-close")).pixmap(16, 16);
+    } else {
+        msg = i18n("Connected successfully");
+        icon = QIcon::fromTheme(QStringLiteral("dialog-ok-apply")).pixmap(16, 16);
+    }
+
+    mStatusLabel->setHtml(QStringLiteral("<html><body><img src=\"icon\"> %1</body></html>").arg(msg));
+    mStatusLabel->document()->addResource(QTextDocument::ImageResource, QUrl(QStringLiteral("icon")), QVariant(icon));
+}
+
diff --git a/resources/dav/resource/setupwizard.h b/resources/dav/resource/setupwizard.h
new file mode 100644 (file)
index 0000000..af3d533
--- /dev/null
@@ -0,0 +1,154 @@
+/*
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef SETUPWIZARD_H
+#define SETUPWIZARD_H
+
+#include "davutils.h"
+
+#include <QWizard>
+#include <QWizardPage>
+#include <qlabel.h>
+
+class KJob;
+class KLineEdit;
+class QTextBrowser;
+
+class QButtonGroup;
+class QCheckBox;
+class QComboBox;
+class QFormLayout;
+class QLabel;
+class QRadioButton;
+
+class SetupWizard : public QWizard
+{
+    Q_OBJECT
+
+public:
+    explicit SetupWizard(QWidget *parent = Q_NULLPTR);
+
+    enum {
+        W_CredentialsPage,
+        W_PredefinedProviderPage,
+        W_ServerTypePage,
+        W_ConnectionPage,
+        W_CheckPage
+    };
+
+    class Url
+    {
+    public:
+        typedef QList<Url> List;
+
+        DavUtils::Protocol protocol;
+        QString url;
+        QString userName;
+        QString password;
+    };
+
+    Url::List urls() const;
+    QString displayName() const;
+};
+
+class PredefinedProviderPage : public QWizardPage
+{
+public:
+    explicit PredefinedProviderPage(QWidget *parent = Q_NULLPTR);
+
+    void initializePage() Q_DECL_OVERRIDE;
+    int nextId() const Q_DECL_OVERRIDE;
+
+private:
+    QLabel *mLabel;
+    QButtonGroup *mProviderGroup;
+    QRadioButton *mUseProvider;
+    QRadioButton *mDontUseProvider;
+};
+
+class CredentialsPage : public QWizardPage
+{
+public:
+    explicit CredentialsPage(QWidget *parent = Q_NULLPTR);
+    int nextId() const Q_DECL_OVERRIDE;
+
+private:
+    KLineEdit *mUserName;
+    KLineEdit *mPassword;
+};
+
+class ServerTypePage : public QWizardPage
+{
+    Q_OBJECT
+
+public:
+    explicit ServerTypePage(QWidget *parent = Q_NULLPTR);
+
+    bool validatePage() Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void manualConfigToggled(bool toggled);
+
+private:
+    QButtonGroup *mServerGroup;
+    QComboBox *mProvidersCombo;
+};
+
+class ConnectionPage : public QWizardPage
+{
+    Q_OBJECT
+
+public:
+    explicit ConnectionPage(QWidget *parent = Q_NULLPTR);
+
+    void initializePage() Q_DECL_OVERRIDE;
+    void cleanupPage() Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void urlElementChanged();
+
+private:
+    QFormLayout *mLayout;
+    KLineEdit *mHost;
+    KLineEdit *mPath;
+    QCheckBox *mUseSecureConnection;
+    QFormLayout *mPreviewLayout;
+    QLabel *mCalDavUrlLabel;
+    QLabel *mCalDavUrlPreview;
+    QLabel *mCardDavUrlLabel;
+    QLabel *mCardDavUrlPreview;
+    QLabel *mGroupDavUrlLabel;
+    QLabel *mGroupDavUrlPreview;
+};
+
+class CheckPage : public QWizardPage
+{
+    Q_OBJECT
+
+public:
+    explicit CheckPage(QWidget *parent = Q_NULLPTR);
+
+private Q_SLOTS:
+    void checkConnection();
+    void onFetchDone(KJob *);
+
+private:
+    QTextBrowser *mStatusLabel;
+};
+
+#endif
diff --git a/resources/dav/resource/urlconfigurationdialog.cpp b/resources/dav/resource/urlconfigurationdialog.cpp
new file mode 100644 (file)
index 0000000..227cf37
--- /dev/null
@@ -0,0 +1,295 @@
+/*
+    Copyright (c) 2010 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "urlconfigurationdialog.h"
+
+#include "davcollectionmodifyjob.h"
+#include "davcollectionsfetchjob.h"
+#include "davutils.h"
+#include "settings.h"
+
+#include <kmessagebox.h>
+#include <KLocalizedString>
+
+#include <KSharedConfigPtr>
+#include <QStandardItem>
+#include <QStandardItemModel>
+#include <QVBoxLayout>
+
+UrlConfigurationDialog::UrlConfigurationDialog(QWidget *parent)
+    : QDialog(parent)
+{
+    QWidget *mainWidget = new QWidget(this);
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    setLayout(mainLayout);
+    mainLayout->addWidget(mainWidget);
+    mUi.setupUi(mainWidget);
+    mUi.credentialsGroup->setVisible(false);
+
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    mOkButton = buttonBox->button(QDialogButtonBox::Ok);
+    mOkButton->setDefault(true);
+    mOkButton->setShortcut(Qt::CTRL | Qt::Key_Return);
+    connect(buttonBox, &QDialogButtonBox::accepted, this, &UrlConfigurationDialog::accept);
+    connect(buttonBox, &QDialogButtonBox::rejected, this, &UrlConfigurationDialog::reject);
+    mainLayout->addWidget(buttonBox);
+
+    mModel = new QStandardItemModel();
+    initModel();
+
+    mUi.discoveredUrls->setModel(mModel);
+    mUi.discoveredUrls->setRootIsDecorated(false);
+    connect(mModel, &QStandardItemModel::dataChanged, this, &UrlConfigurationDialog::onModelDataChanged);
+
+    connect(mUi.remoteProtocol, &KButtonGroup::changed, this, &UrlConfigurationDialog::onConfigChanged);
+    connect(mUi.remoteUrl, &KLineEdit::textChanged, this, &UrlConfigurationDialog::onConfigChanged);
+    connect(mUi.useDefaultCreds, &QRadioButton::toggled, this, &UrlConfigurationDialog::onConfigChanged);
+    connect(mUi.username, &KLineEdit::textChanged, this, &UrlConfigurationDialog::onConfigChanged);
+    connect(mUi.password, &KLineEdit::textChanged, this, &UrlConfigurationDialog::onConfigChanged);
+
+    connect(mUi.fetchButton, &QPushButton::clicked, this, &UrlConfigurationDialog::onFetchButtonClicked);
+    connect(mOkButton, &QPushButton::clicked, this, &UrlConfigurationDialog::onOkButtonClicked);
+
+    checkUserInput();
+    readConfig();
+}
+
+UrlConfigurationDialog::~UrlConfigurationDialog()
+{
+    writeConfig();
+}
+
+void UrlConfigurationDialog::readConfig()
+{
+    KConfigGroup grp(KSharedConfig::openConfig(), "UrlConfigurationDialog");
+    const QSize size = grp.readEntry("Size", QSize(300, 200));
+    if (size.isValid()) {
+        resize(size);
+    }
+}
+
+void UrlConfigurationDialog::writeConfig()
+{
+    KConfigGroup grp(KSharedConfig::openConfig(), "UrlConfigurationDialog");
+    grp.writeEntry("Size", size());
+    grp.sync();
+}
+
+DavUtils::Protocol UrlConfigurationDialog::protocol() const
+{
+    return DavUtils::Protocol(mUi.remoteProtocol->selected());
+}
+
+void UrlConfigurationDialog::setProtocol(DavUtils::Protocol protocol)
+{
+    mUi.remoteProtocol->setSelected(protocol);
+}
+
+QString UrlConfigurationDialog::remoteUrl() const
+{
+    return mUi.remoteUrl->text();
+}
+
+void UrlConfigurationDialog::setRemoteUrl(const QString &url)
+{
+    mUi.remoteUrl->setText(url);
+}
+
+bool UrlConfigurationDialog::useDefaultCredentials() const
+{
+    return mUi.useDefaultCreds->isChecked();
+}
+
+void UrlConfigurationDialog::setUseDefaultCredentials(bool defaultCreds)
+{
+    if (defaultCreds) {
+        mUi.useDefaultCreds->setChecked(true);
+    } else {
+        mUi.useSpecificCreds->setChecked(true);
+    }
+}
+
+QString UrlConfigurationDialog::username() const
+{
+    if (mUi.useDefaultCreds->isChecked()) {
+        return mDefaultUsername;
+    } else {
+        return mUi.username->text();
+    }
+}
+
+void UrlConfigurationDialog::setDefaultUsername(const QString &userName)
+{
+    mDefaultUsername = userName;
+}
+
+void UrlConfigurationDialog::setUsername(const QString &userName)
+{
+    mUi.username->setText(userName);
+}
+
+QString UrlConfigurationDialog::password() const
+{
+    if (mUi.useDefaultCreds->isChecked()) {
+        return mDefaultPassword;
+    } else {
+        return mUi.password->text();
+    }
+}
+
+void UrlConfigurationDialog::setDefaultPassword(const QString &password)
+{
+    mDefaultPassword = password;
+}
+
+void UrlConfigurationDialog::setPassword(const QString &password)
+{
+    mUi.password->setText(password);
+}
+
+void UrlConfigurationDialog::onConfigChanged()
+{
+    initModel();
+    mUi.fetchButton->setEnabled(false);
+    mOkButton->setEnabled(false);
+    checkUserInput();
+}
+
+void UrlConfigurationDialog::checkUserInput()
+{
+    if (!mUi.remoteUrl->text().trimmed().isEmpty() && checkUserAuthInput()) {
+        mUi.fetchButton->setEnabled(true);
+        if (mModel->rowCount() > 0) {
+            mOkButton->setEnabled(true);
+        }
+    } else {
+        mUi.fetchButton->setEnabled(false);
+        mOkButton->setEnabled(false);
+    }
+}
+
+void UrlConfigurationDialog::onFetchButtonClicked()
+{
+    mUi.discoveredUrls->setEnabled(false);
+    initModel();
+
+    if (!remoteUrl().endsWith(QLatin1Char('/'))) {
+        setRemoteUrl(remoteUrl() + QLatin1Char('/'));
+    }
+
+    if (!remoteUrl().startsWith(QStringLiteral("https://")) && !remoteUrl().startsWith(QStringLiteral("http://"))) {
+        setRemoteUrl(QStringLiteral("https://") + remoteUrl());
+    }
+
+    QUrl url(mUi.remoteUrl->text());
+    if (mUi.useDefaultCreds->isChecked()) {
+        url.setUserName(mDefaultUsername);
+        url.setPassword(mDefaultPassword);
+    } else {
+        url.setUserName(username());
+        url.setPassword(password());
+    }
+
+    DavUtils::DavUrl davUrl(url, protocol());
+    DavCollectionsFetchJob *job = new DavCollectionsFetchJob(davUrl);
+    connect(job, &DavCollectionsFetchJob::result, this, &UrlConfigurationDialog::onCollectionsFetchDone);
+    job->start();
+}
+
+void UrlConfigurationDialog::onOkButtonClicked()
+{
+    if (!remoteUrl().endsWith(QLatin1Char('/'))) {
+        setRemoteUrl(remoteUrl() + QLatin1Char('/'));
+    }
+}
+
+void UrlConfigurationDialog::onCollectionsFetchDone(KJob *job)
+{
+    mUi.discoveredUrls->setEnabled(true);
+
+    if (job->error()) {
+        KMessageBox::error(this, job->errorText());
+        return;
+    }
+
+    DavCollectionsFetchJob *davJob = qobject_cast<DavCollectionsFetchJob *>(job);
+
+    const DavCollection::List collections = davJob->collections();
+
+    foreach (const DavCollection &collection, collections) {
+        addModelRow(collection.displayName(), collection.url());
+    }
+
+    checkUserInput();
+}
+
+void UrlConfigurationDialog::onModelDataChanged(const QModelIndex &topLeft, const QModelIndex &)
+{
+    // Actually only the display name can be changed, so no stricts checks are required
+    const QString newName = topLeft.data().toString();
+    const QString url = topLeft.sibling(topLeft.row(), 1).data().toString();
+
+    QUrl fullUrl(url);
+    fullUrl.setUserInfo(QString());
+
+    DavUtils::DavUrl davUrl(fullUrl, protocol());
+    DavCollectionModifyJob *job = new DavCollectionModifyJob(davUrl);
+    job->setProperty(QStringLiteral("displayname"), newName);
+    connect(job, &DavCollectionModifyJob::result, this, &UrlConfigurationDialog::onChangeDisplayNameFinished);
+    job->start();
+    mUi.discoveredUrls->setEnabled(false);
+}
+
+void UrlConfigurationDialog::onChangeDisplayNameFinished(KJob *job)
+{
+    if (job->error()) {
+        KMessageBox::error(this, job->errorText());
+    }
+
+    onFetchButtonClicked();
+}
+
+void UrlConfigurationDialog::initModel()
+{
+    mModel->clear();
+    QStringList headers;
+    headers << i18n("Display name") << i18n("URL");
+    mModel->setHorizontalHeaderLabels(headers);
+}
+
+bool UrlConfigurationDialog::checkUserAuthInput()
+{
+    return (mUi.useDefaultCreds->isChecked() || !(mUi.username->text().isEmpty() || mUi.password->text().isEmpty()));
+}
+
+void UrlConfigurationDialog::addModelRow(const QString &displayName, const QString &url)
+{
+    QStandardItem *rootItem = mModel->invisibleRootItem();
+
+    QList<QStandardItem *> items;
+
+    QStandardItem *displayNameStandardItem = new QStandardItem(displayName);
+    displayNameStandardItem->setEditable(true);
+    items << displayNameStandardItem;
+
+    QStandardItem *urlStandardItem = new QStandardItem(url);
+    urlStandardItem->setEditable(false);
+    items << urlStandardItem;
+
+    rootItem->appendRow(items);
+}
diff --git a/resources/dav/resource/urlconfigurationdialog.h b/resources/dav/resource/urlconfigurationdialog.h
new file mode 100644 (file)
index 0000000..7181bdf
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+    Copyright (c) 2010 Grégory Oestreicher <greg@kamago.net>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef URLCONFIGURATIONDIALOG_H
+#define URLCONFIGURATIONDIALOG_H
+
+#include "ui_urlconfigurationdialog.h"
+
+#include "davutils.h"
+
+#include <QtCore/QString>
+#include <QDialog>
+
+class KJob;
+class QModelIndex;
+class QStandardItemModel;
+class QPushButton;
+class UrlConfigurationDialog : public QDialog
+{
+    Q_OBJECT
+
+public:
+    explicit UrlConfigurationDialog(QWidget *parent = Q_NULLPTR);
+    ~UrlConfigurationDialog();
+
+    DavUtils::Protocol protocol() const;
+    void setProtocol(DavUtils::Protocol protocol);
+
+    QString remoteUrl() const;
+    void setRemoteUrl(const QString &url);
+
+    bool useDefaultCredentials() const;
+    void setUseDefaultCredentials(bool defaultCreds);
+
+    QString username() const;
+    void setDefaultUsername(const QString &name);
+    void setUsername(const QString &name);
+
+    QString password() const;
+    void setDefaultPassword(const QString &password);
+    void setPassword(const QString &password);
+
+private Q_SLOTS:
+    void onConfigChanged();
+    void checkUserInput();
+    void onFetchButtonClicked();
+    void onOkButtonClicked();
+    void onCollectionsFetchDone(KJob *job);
+    void onModelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight);
+    void onChangeDisplayNameFinished(KJob *job);
+
+private:
+    void initModel();
+    bool checkUserAuthInput();
+    void addModelRow(const QString &displayName, const QString &url);
+    void writeConfig();
+    void readConfig();
+
+    Ui::UrlConfigurationDialog mUi;
+    QStandardItemModel *mModel;
+    QString mDefaultUsername;
+    QString mDefaultPassword;
+    QPushButton *mOkButton;
+};
+
+#endif
diff --git a/resources/dav/resource/urlconfigurationdialog.ui b/resources/dav/resource/urlconfigurationdialog.ui
new file mode 100644 (file)
index 0000000..f7a751b
--- /dev/null
@@ -0,0 +1,232 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>UrlConfigurationDialog</class>
+ <widget class="QWidget" name="UrlConfigurationDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>559</width>
+    <height>698</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="locale">
+   <locale language="English" country="UnitedStates"/>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_2">
+   <item>
+    <widget class="KButtonGroup" name="remoteProtocol">
+     <property name="locale">
+      <locale language="English" country="UnitedStates"/>
+     </property>
+     <property name="title">
+      <string>Remote calendar access protocol</string>
+     </property>
+     <property name="current" stdset="0">
+      <number>0</number>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout">
+      <item>
+       <widget class="QRadioButton" name="caldav">
+        <property name="text">
+         <string>CalDAV</string>
+        </property>
+        <property name="checked">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QRadioButton" name="carddav">
+        <property name="text">
+         <string>CardDAV</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QRadioButton" name="groupdav">
+        <property name="text">
+         <string>GroupDAV</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="groupBox_2">
+     <property name="locale">
+      <locale language="English" country="UnitedStates"/>
+     </property>
+     <property name="title">
+      <string>Remote calendar access</string>
+     </property>
+     <layout class="QFormLayout" name="formLayout_2">
+      <item row="0" column="1">
+       <widget class="KLineEdit" name="remoteUrl"/>
+      </item>
+      <item row="1" column="0" colspan="2">
+       <widget class="QRadioButton" name="useDefaultCreds">
+        <property name="text">
+         <string>Use global credentials</string>
+        </property>
+        <property name="checked">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item row="2" column="0" colspan="2">
+       <widget class="QRadioButton" name="useSpecificCreds">
+        <property name="text">
+         <string>Use specific credentials</string>
+        </property>
+       </widget>
+      </item>
+      <item row="3" column="0" colspan="2">
+       <widget class="QGroupBox" name="credentialsGroup">
+        <property name="title">
+         <string>Credentials</string>
+        </property>
+        <layout class="QFormLayout" name="formLayout">
+         <item row="0" column="0">
+          <widget class="QLabel" name="label_4">
+           <property name="toolTip">
+            <string comment="The user's login on the remote server"/>
+           </property>
+           <property name="locale">
+            <locale language="English" country="UnitedStates"/>
+           </property>
+           <property name="text">
+            <string>Username</string>
+           </property>
+          </widget>
+         </item>
+         <item row="0" column="1">
+          <widget class="KLineEdit" name="username"/>
+         </item>
+         <item row="1" column="0">
+          <widget class="QLabel" name="label_5">
+           <property name="toolTip">
+            <string comment="The user's login on the remote server"/>
+           </property>
+           <property name="locale">
+            <locale language="English" country="UnitedStates"/>
+           </property>
+           <property name="text">
+            <string>Password</string>
+           </property>
+          </widget>
+         </item>
+         <item row="1" column="1">
+          <widget class="KLineEdit" name="password">
+           <property name="echoMode">
+            <enum>QLineEdit::Password</enum>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </widget>
+      </item>
+      <item row="0" column="0">
+       <widget class="QLabel" name="label">
+        <property name="text">
+         <string>Remote URL</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="groupBox">
+     <property name="locale">
+      <locale language="English" country="UnitedStates"/>
+     </property>
+     <property name="title">
+      <string>Discovered collections</string>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout_3">
+      <item>
+       <layout class="QHBoxLayout" name="horizontalLayout_2">
+        <item>
+         <spacer name="horizontalSpacer">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item>
+         <widget class="QPushButton" name="fetchButton">
+          <property name="text">
+           <string>Fetch</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <spacer name="horizontalSpacer_3">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+       </layout>
+      </item>
+      <item>
+       <widget class="QTreeView" name="discoveredUrls"/>
+      </item>
+     </layout>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KButtonGroup</class>
+   <extends>QGroupBox</extends>
+   <header>kbuttongroup.h</header>
+   <container>1</container>
+  </customwidget>
+  <customwidget>
+   <class>KLineEdit</class>
+   <extends>QLineEdit</extends>
+   <header>klineedit.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>useSpecificCreds</sender>
+   <signal>toggled(bool)</signal>
+   <receiver>credentialsGroup</receiver>
+   <slot>setVisible(bool)</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>22</x>
+     <y>206</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>59</x>
+     <y>241</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
diff --git a/resources/dav/services/citadel.desktop b/resources/dav/services/citadel.desktop
new file mode 100644 (file)
index 0000000..5e253cc
--- /dev/null
@@ -0,0 +1,50 @@
+[Desktop Entry]
+Name=Citadel
+Name[bg]=Citadel
+Name[bs]=Citadel
+Name[ca]=Citadel
+Name[ca@valencia]=Citadel
+Name[cs]=Citadel
+Name[da]=Citadel
+Name[de]=Citadel
+Name[el]=Citadel
+Name[en_GB]=Citadel
+Name[es]=Citadel
+Name[et]=Citadel
+Name[fi]=Citadel
+Name[fr]=Citadel
+Name[ga]=Citadel
+Name[gl]=Citadel
+Name[hu]=Citadel
+Name[ia]=Citadel
+Name[it]=Citadel
+Name[kk]=Citadel
+Name[km]=Citadel
+Name[ko]=Citadel
+Name[lt]=Citadel
+Name[lv]=Citadel
+Name[nb]=Citadel
+Name[nds]=Citadel
+Name[nl]=Citadel
+Name[pl]=Citadel
+Name[pt]=Citadel
+Name[pt_BR]=Citadel
+Name[ro]=Citadel
+Name[ru]=Citadel
+Name[sk]=Citadel
+Name[sl]=Citadel
+Name[sr]=Цитадела
+Name[sr@ijekavian]=Цитадела
+Name[sr@ijekavianlatin]=Citadela
+Name[sr@latin]=Citadela
+Name[sv]=Citadel
+Name[tr]=Citadel
+Name[uk]=Citadel
+Name[x-test]=xxCitadelxx
+Name[zh_CN]=Citadel
+Name[zh_TW]=Citadel
+Type=Service
+X-KDE-ServiceTypes=DavGroupwareProvider
+
+X-DavGroupware-SupportedProtocols=GroupDav
+X-DavGroupware-GroupDavPath=/groupdav
diff --git a/resources/dav/services/davical.desktop b/resources/dav/services/davical.desktop
new file mode 100644 (file)
index 0000000..31d9b7f
--- /dev/null
@@ -0,0 +1,52 @@
+[Desktop Entry]
+Name=Davical
+Name[bg]=Davical
+Name[bs]=Davical
+Name[ca]=Davical
+Name[ca@valencia]=Davical
+Name[cs]=Davical
+Name[da]=Davical
+Name[de]=Davical
+Name[el]=Davical
+Name[en_GB]=Davical
+Name[es]=Davical
+Name[et]=Davical
+Name[fi]=Davical
+Name[fr]=Davical
+Name[ga]=Davical
+Name[gl]=Davical
+Name[hu]=Davical
+Name[ia]=Davical
+Name[it]=Davical
+Name[kk]=Davical
+Name[km]=Davical
+Name[ko]=Davical
+Name[lt]=Davical
+Name[lv]=Davical
+Name[nb]=Davical
+Name[nds]=DAVical
+Name[nl]=Davical
+Name[pl]=Davical
+Name[pt]=Davical
+Name[pt_BR]=Davical
+Name[ro]=Davical
+Name[ru]=DAViCal
+Name[sk]=Davical
+Name[sl]=Davical
+Name[sr]=ДАВикал
+Name[sr@ijekavian]=ДАВикал
+Name[sr@ijekavianlatin]=DAViCal
+Name[sr@latin]=DAViCal
+Name[sv]=Davical
+Name[tr]=Davical
+Name[uk]=Davical
+Name[x-test]=xxDavicalxx
+Name[zh_CN]=Davical
+Name[zh_TW]=Davical
+Type=Service
+X-KDE-ServiceTypes=DavGroupwareProvider
+
+X-DavGroupware-Provider=davical
+X-DavGroupware-SupportedProtocols=CalDav
+X-DavGroupware-CalDavPath=/caldav.php
+
diff --git a/resources/dav/services/egroupware.desktop b/resources/dav/services/egroupware.desktop
new file mode 100644 (file)
index 0000000..ddde6f9
--- /dev/null
@@ -0,0 +1,52 @@
+[Desktop Entry]
+Name=eGroupware
+Name[bg]=eGroupware
+Name[bs]=eGroupware
+Name[ca]=eGroupware
+Name[ca@valencia]=eGroupware
+Name[cs]=eGroupware
+Name[da]=eGroupware
+Name[de]=eGroupware
+Name[el]=eGroupware
+Name[en_GB]=eGroupware
+Name[es]=eGroupware
+Name[et]=eGroupware
+Name[fi]=eGroupware
+Name[fr]=eGroupware
+Name[ga]=eGroupware
+Name[gl]=eGroupware
+Name[hu]=eGroupware
+Name[ia]=eGroupware
+Name[it]=eGroupware
+Name[kk]=eGroupware
+Name[km]=eGroupware
+Name[ko]=eGroupware
+Name[lt]=eGroupware
+Name[lv]=eGroupware
+Name[mr]=इ-ग्रुपवेअर
+Name[nb]=eGroupware
+Name[nds]=eGroupware
+Name[nl]=eGroupware
+Name[pl]=eGroupware
+Name[pt]=eGroupware
+Name[pt_BR]=eGroupware
+Name[ro]=eGroupware
+Name[ru]=eGroupware
+Name[sk]=eGroupware
+Name[sl]=eGroupware
+Name[sr]=Е‑групвер
+Name[sr@ijekavian]=Е‑групвер
+Name[sr@ijekavianlatin]=EGroupware
+Name[sr@latin]=EGroupware
+Name[sv]=eGroupware
+Name[tr]=eGroupware
+Name[uk]=eGroupware
+Name[x-test]=xxeGroupwarexx
+Name[zh_CN]=eGroupware
+Name[zh_TW]=eGroupware
+Type=Service
+X-KDE-ServiceTypes=DavGroupwareProvider
+
+X-DavGroupware-SupportedProtocols=CalDav,CardDav
+X-DavGroupware-CalDavPath=/groupdav.php
+X-DavGroupware-CardDavPath=/groupdav.php
diff --git a/resources/dav/services/opengroupware.desktop b/resources/dav/services/opengroupware.desktop
new file mode 100644 (file)
index 0000000..9c57048
--- /dev/null
@@ -0,0 +1,51 @@
+[Desktop Entry]
+Name=OpenGroupware
+Name[bg]=OpenGroupware
+Name[bs]=OpenGroupware
+Name[ca]=OpenGroupware
+Name[ca@valencia]=OpenGroupware
+Name[cs]=OpenGroupware
+Name[da]=OpenGroupware
+Name[de]=OpenGroupware
+Name[el]=OpenGroupware
+Name[en_GB]=OpenGroupware
+Name[es]=OpenGroupware
+Name[et]=OpenGroupware
+Name[fi]=OpenGroupware
+Name[fr]=OpenGroupware
+Name[ga]=OpenGroupware
+Name[gl]=OpenGroupware
+Name[hu]=OpenGroupware
+Name[ia]=OpenGroupware
+Name[it]=OpenGroupware
+Name[kk]=OpenGroupware
+Name[km]=OpenGroupware
+Name[ko]=OpenGroupware
+Name[lt]=OpenGroupware
+Name[lv]=OpenGroupware
+Name[mr]=ओपन-ग्रुपवेअर
+Name[nb]=OpenGroupware
+Name[nds]=OpenGroupware
+Name[nl]=OpenGroupware
+Name[pl]=OpenGroupware
+Name[pt]=OpenGroupware
+Name[pt_BR]=OpenGroupware
+Name[ro]=OpenGroupware
+Name[ru]=OpenGroupware
+Name[sk]=OpenGroupware
+Name[sl]=OpenGroupware
+Name[sr]=Опенгрупвер
+Name[sr@ijekavian]=Опенгрупвер
+Name[sr@ijekavianlatin]=OpenGroupware
+Name[sr@latin]=OpenGroupware
+Name[sv]=OpenGroupware
+Name[tr]=OpenGroupware
+Name[uk]=OpenGroupware
+Name[x-test]=xxOpenGroupwarexx
+Name[zh_CN]=OpenGroupware
+Name[zh_TW]=OpenGroupware
+Type=Service
+X-KDE-ServiceTypes=DavGroupwareProvider
+
+X-DavGroupware-SupportedProtocols=GroupDav
+X-DavGroupware-GroupDavPath=/zidestore/dav/$user
diff --git a/resources/dav/services/owncloud-pre5.desktop b/resources/dav/services/owncloud-pre5.desktop
new file mode 100644 (file)
index 0000000..70a735e
--- /dev/null
@@ -0,0 +1,48 @@
+[Desktop Entry]
+Name=ownCloud (< 5.0)
+Name[ast]=ownCloud (< 5.0)
+Name[bg]=ownCloud (< 5.0)
+Name[bs]=ownCloud (< 5.0)
+Name[ca]=ownCloud (< 5.0)
+Name[ca@valencia]=ownCloud (< 5.0)
+Name[cs]=ownCloud (< 5.0)
+Name[da]=ownCloud (< 5.0)
+Name[de]=ownCloud (< 5.0)
+Name[el]=ownCloud (<5.0)
+Name[en_GB]=ownCloud (< 5.0)
+Name[es]=ownCloud (< 5.0)
+Name[et]=ownCloud (< 5.0)
+Name[fi]=ownCloud (< 5.0)
+Name[fr]=ownCloud (< 5.0)
+Name[gl]=ownCloud (< 5.0)
+Name[hu]=ownCloud (< 5.0)
+Name[ia]=ownCloud (<5.0)
+Name[it]=ownCloud (< 5.0)
+Name[kk]=ownCloud (< 5.0)
+Name[ko]=ownCloud (< 5.0)
+Name[lt]=ownCloud (< 5.0)
+Name[nb]=ownCloud (< 5.0)
+Name[nds]=ownCloud (< 5.0)
+Name[nl]=ownCloud (< 5.0)
+Name[pl]=ownCloud (< 5.0)
+Name[pt]=ownCloud (< 5.0)
+Name[pt_BR]=ownCloud (< 5.0)
+Name[ru]=ownCloud (< 5.0)
+Name[sk]=ownCloud (< 5.0)
+Name[sl]=ownCloud (< 5.0)
+Name[sr]=Оунклауд (< 5.0)
+Name[sr@ijekavian]=Оунклауд (< 5.0)
+Name[sr@ijekavianlatin]=ownCloud (< 5.0)
+Name[sr@latin]=ownCloud (< 5.0)
+Name[sv]=ownCloud (< 5.0)
+Name[tr]=ownCloud (< 5.0)
+Name[uk]=ownCloud (< 5.0)
+Name[x-test]=xxownCloud (< 5.0)xx
+Name[zh_CN]=ownCloud (< 5.0)
+Name[zh_TW]=ownCloud (< 5.0)
+Type=Service
+X-KDE-ServiceTypes=DavGroupwareProvider
+
+X-DavGroupware-SupportedProtocols=CalDav,CardDav
+X-DavGroupware-CalDavPath=/apps/calendar/caldav.php
+X-DavGroupware-CardDavPath=/apps/contacts/carddav.php
diff --git a/resources/dav/services/owncloud.desktop b/resources/dav/services/owncloud.desktop
new file mode 100644 (file)
index 0000000..b25e382
--- /dev/null
@@ -0,0 +1,52 @@
+[Desktop Entry]
+Name=ownCloud
+Name[ast]=ownCloud
+Name[bg]=ownCloud
+Name[bs]=ownCloud
+Name[ca]=ownCloud
+Name[ca@valencia]=ownCloud
+Name[cs]=ownCloud
+Name[da]=ownCloud
+Name[de]=ownCloud
+Name[el]=ownCloud
+Name[en_GB]=ownCloud
+Name[es]=ownCloud
+Name[et]=ownCloud
+Name[fi]=ownCloud
+Name[fr]=ownCloud
+Name[gl]=ownCloud
+Name[hu]=ownCloud
+Name[ia]=proprie Nube (ownCloud)
+Name[it]=ownCloud
+Name[kk]=ownCloud
+Name[km]=ownCloud
+Name[ko]=ownCloud
+Name[lt]=ownCloud
+Name[lv]=ownCloud
+Name[mr]=ओन-क्लाउड
+Name[nb]=ownCloud
+Name[nds]=ownCloud
+Name[nl]=ownCloud
+Name[pl]=ownCloud
+Name[pt]=ownCloud
+Name[pt_BR]=ownCloud
+Name[ru]=ownCloud
+Name[sk]=ownCloud
+Name[sl]=ownCloud
+Name[sr]=Оунклауд
+Name[sr@ijekavian]=Оунклауд
+Name[sr@ijekavianlatin]=ownCloud
+Name[sr@latin]=ownCloud
+Name[sv]=ownCloud
+Name[tr]=ownCloud
+Name[ug]=ownCloud
+Name[uk]=ownCloud
+Name[x-test]=xxownCloudxx
+Name[zh_CN]=ownCloud
+Name[zh_TW]=ownCloud
+Type=Service
+X-KDE-ServiceTypes=DavGroupwareProvider
+
+X-DavGroupware-SupportedProtocols=CalDav,CardDav
+X-DavGroupware-CalDavPath=/remote.php/caldav
+X-DavGroupware-CardDavPath=/remote.php/carddav
diff --git a/resources/dav/services/scalix.desktop b/resources/dav/services/scalix.desktop
new file mode 100644 (file)
index 0000000..be1de20
--- /dev/null
@@ -0,0 +1,52 @@
+[Desktop Entry]
+Name=Scalix
+Name[ast]=Scalix
+Name[bg]=Scalix
+Name[bs]=Scalix
+Name[ca]=Scalix
+Name[ca@valencia]=Scalix
+Name[cs]=Scalix
+Name[da]=Scalix
+Name[de]=Scalix
+Name[el]=Scalix
+Name[en_GB]=Scalix
+Name[es]=Scalix
+Name[et]=Scalix
+Name[fi]=Scalix
+Name[fr]=Scalix
+Name[ga]=Scalix
+Name[gl]=Scalix
+Name[hu]=Scalix
+Name[ia]=Scalix
+Name[it]=Scalix
+Name[kk]=Scalix
+Name[km]=Scalix
+Name[ko]=Scalix
+Name[lt]=Scalix
+Name[lv]=Scalix
+Name[mr]=स्केलिक्स
+Name[nb]=Scalix
+Name[nds]=Scalix
+Name[nl]=Scalix
+Name[pl]=Scalix
+Name[pt]=Scalix
+Name[pt_BR]=Scalix
+Name[ro]=Scalix
+Name[ru]=Scalix
+Name[sk]=Scalix
+Name[sl]=Scalix
+Name[sr]=Скejликс
+Name[sr@ijekavian]=Скejликс
+Name[sr@ijekavianlatin]=Scalix
+Name[sr@latin]=Scalix
+Name[sv]=Scalix
+Name[tr]=Scalix
+Name[uk]=Scalix
+Name[x-test]=xxScalixxx
+Name[zh_CN]=Scalix
+Name[zh_TW]=Scalix
+Type=Service
+X-KDE-ServiceTypes=DavGroupwareProvider
+
+X-DavGroupware-SupportedProtocols=CalDav
+X-DavGroupware-CalDavPath=/api/dav/Principals/$user$
diff --git a/resources/dav/services/sogo.desktop b/resources/dav/services/sogo.desktop
new file mode 100644 (file)
index 0000000..631d166
--- /dev/null
@@ -0,0 +1,53 @@
+[Desktop Entry]
+Name=ScalableOGo
+Name[ast]=ScalableOGo
+Name[bg]=ScalableOGo
+Name[bs]=ScalableOGo
+Name[ca]=ScalableOGo
+Name[ca@valencia]=ScalableOGo
+Name[cs]=ScalableOGo
+Name[da]=ScalableOGo
+Name[de]=ScalableOGo
+Name[el]=ScalableOGo
+Name[en_GB]=ScalableOGo
+Name[es]=ScalableOGo
+Name[et]=ScalableOGo
+Name[fi]=ScalableOGo
+Name[fr]=ScalableOGo
+Name[ga]=ScalableOGo
+Name[gl]=ScalableOGo
+Name[hu]=ScalableOGo
+Name[ia]=ScalableOGo
+Name[it]=ScalableOGo
+Name[kk]=ScalableOGo
+Name[km]=ScalableOGo
+Name[ko]=ScalableOGo
+Name[lt]=ScalableOGo
+Name[lv]=ScalableOGo
+Name[nb]=ScalableOGo
+Name[nds]=ScalableOGo
+Name[nl]=ScalableOGo
+Name[pl]=ScalableOGo
+Name[pt]=ScalableOGo
+Name[pt_BR]=ScalableOGo
+Name[ro]=ScalableOGo
+Name[ru]=ScalableOGo
+Name[sk]=ScalableOGo
+Name[sl]=ScalableOGo
+Name[sr]=Скејлабл‑ого
+Name[sr@ijekavian]=Скејлабл‑ого
+Name[sr@ijekavianlatin]=ScalableOGo
+Name[sr@latin]=ScalableOGo
+Name[sv]=ScalableOGo
+Name[tr]=ScalableOGo
+Name[uk]=ScalableOGo
+Name[x-test]=xxScalableOGoxx
+Name[zh_CN]=ScalableOGo
+Name[zh_TW]=ScalableOGo
+Type=Service
+X-KDE-ServiceTypes=DavGroupwareProvider
+
+X-DavGroupware-SupportedProtocols=CalDav,CardDav
+X-DavGroupware-InstallationPath=/SOGo
+X-DavGroupware-CalDavPath=/dav/$user$/Calendar
+X-DavGroupware-CardDavPath=/dav/$user$/Contacts
diff --git a/resources/dav/services/yahoo.desktop b/resources/dav/services/yahoo.desktop
new file mode 100644 (file)
index 0000000..0a73941
--- /dev/null
@@ -0,0 +1,57 @@
+[Desktop Entry]
+Name=Yahoo!
+Name[ast]=Yahoo!
+Name[bg]=Yahoo!
+Name[bs]=Yahoo!
+Name[ca]=Yahoo!
+Name[ca@valencia]=Yahoo!
+Name[cs]=Yahoo!
+Name[da]=Yahoo!
+Name[de]=Yahoo!
+Name[el]=Yahoo!
+Name[en_GB]=Yahoo!
+Name[es]=Yahoo!
+Name[et]=Yahoo!
+Name[fi]=Yahoo!
+Name[fr]=Yahoo!
+Name[ga]=Yahoo!
+Name[gl]=Yahoo!
+Name[hu]=Yahoo!
+Name[ia]=Yahoo!
+Name[it]=Yahoo!
+Name[kk]=Yahoo!
+Name[km]=Yahoo!
+Name[ko]=Yahoo!
+Name[lt]=Yahoo!
+Name[lv]=Yahoo!
+Name[mr]=याहू!
+Name[nb]=Yahoo!
+Name[nds]=Yahoo!
+Name[nl]=Yahoo!
+Name[pl]=Yahoo!
+Name[pt]=Yahoo!
+Name[pt_BR]=Yahoo!
+Name[ro]=Yahoo!
+Name[ru]=Yahoo!
+Name[sk]=Yahoo!
+Name[sl]=Yahoo!
+Name[sr]=Јаху
+Name[sr@ijekavian]=Јаху
+Name[sr@ijekavianlatin]=Yahoo
+Name[sr@latin]=Yahoo
+Name[sv]=Yahoo!
+Name[tr]=Yahoo!
+Name[ug]=Yahoo!
+Name[uk]=Yahoo!
+Name[x-test]=xxYahoo!xx
+Name[zh_CN]=Yahoo!
+Name[zh_TW]=Yahoo!
+Type=Service
+X-KDE-ServiceTypes=DavGroupwareProvider
+
+X-DavGroupware-SupportedProtocols=CalDav,CardDav
+X-DavGroupware-ProviderUsesSSL=1
+X-DavGroupware-CalDavHost=caldav.calendar.yahoo.com
+X-DavGroupware-CalDavPath=/principals/users/$localpart$
+X-DavGroupware-CardDavHost=carddav.address.yahoo.com
+X-DavGroupware-CardDavPath=/principals/users/$localpart$
diff --git a/resources/dav/services/zarafa.desktop b/resources/dav/services/zarafa.desktop
new file mode 100644 (file)
index 0000000..494aaea
--- /dev/null
@@ -0,0 +1,52 @@
+[Desktop Entry]
+Name=Zarafa
+Name[ast]=Zarafa
+Name[bg]=Zarafa
+Name[bs]=Zarafa
+Name[ca]=Zarafa
+Name[ca@valencia]=Zarafa
+Name[cs]=Zarafa
+Name[da]=Zarafa
+Name[de]=Zarafa
+Name[el]=Zarafa
+Name[en_GB]=Zarafa
+Name[es]=Zarafa
+Name[et]=Zarafa
+Name[fi]=Zarafa
+Name[fr]=Zarafa
+Name[ga]=Zarafa
+Name[gl]=Zarafa
+Name[hu]=Zarafa
+Name[ia]=Zarafa
+Name[it]=Zarafa
+Name[kk]=Zarafa
+Name[km]=Zarafa
+Name[ko]=Zarafa
+Name[lt]=Zarafa
+Name[lv]=Zarafa
+Name[mr]=झराफा
+Name[nb]=Zarafa
+Name[nds]=Zarafa
+Name[nl]=Zarafa
+Name[pl]=Zarafa
+Name[pt]=Zarafa
+Name[pt_BR]=Zarafa
+Name[ro]=Zarafa
+Name[ru]=Zarafa
+Name[sk]=Zarafa
+Name[sl]=Zarafa
+Name[sr]=Зарафа
+Name[sr@ijekavian]=Зарафа
+Name[sr@ijekavianlatin]=Zarafa
+Name[sr@latin]=Zarafa
+Name[sv]=Zarafa
+Name[tr]=Zarafa
+Name[uk]=Zarafa
+Name[x-test]=xxZarafaxx
+Name[zh_CN]=Zarafa
+Name[zh_TW]=Zarafa
+Type=Service
+X-KDE-ServiceTypes=DavGroupwareProvider
+
+X-DavGroupware-SupportedProtocols=CalDav
+X-DavGroupware-CalDavPath=/caldav/$user$
diff --git a/resources/dav/services/zimbra.desktop b/resources/dav/services/zimbra.desktop
new file mode 100644 (file)
index 0000000..b188d01
--- /dev/null
@@ -0,0 +1,53 @@
+[Desktop Entry]
+Name=Zimbra
+Name[ast]=Zimbra
+Name[bg]=Zimbra
+Name[bs]=Zimbra
+Name[ca]=Zimbra
+Name[ca@valencia]=Zimbra
+Name[cs]=Zimbra
+Name[da]=Zimbra
+Name[de]=Zimbra
+Name[el]=Zimbra
+Name[en_GB]=Zimbra
+Name[es]=Zimbra
+Name[et]=Zimbra
+Name[fi]=Zimbra
+Name[fr]=Zimbra
+Name[ga]=Zimbra
+Name[gl]=Zimbra
+Name[hu]=Zimbra
+Name[ia]=Zimbra
+Name[it]=Zimbra
+Name[kk]=Zimbra
+Name[km]=Zimbra
+Name[ko]=Zimbra
+Name[lt]=Zimbra
+Name[lv]=Zimbra
+Name[mr]=झिंब्रा
+Name[nb]=Zimbra
+Name[nds]=Zimbra
+Name[nl]=Zimbra
+Name[pl]=Zimbra
+Name[pt]=Zimbra
+Name[pt_BR]=Zimbra
+Name[ro]=Zimbra
+Name[ru]=Zimbra
+Name[sk]=Zimbra
+Name[sl]=Zimbra
+Name[sr]=Зимбра
+Name[sr@ijekavian]=Зимбра
+Name[sr@ijekavianlatin]=Zimbra
+Name[sr@latin]=Zimbra
+Name[sv]=Zimbra
+Name[tr]=Zimbra
+Name[uk]=Zimbra
+Name[x-test]=xxZimbraxx
+Name[zh_CN]=Zimbra
+Name[zh_TW]=Zimbra
+Type=Service
+X-KDE-ServiceTypes=DavGroupwareProvider
+
+X-DavGroupware-SupportedProtocols=CalDav,CardDav
+X-DavGroupware-CalDavPath=/principals/users/$user$
+X-DavGroupware-CardDavPath=/principals/users/$user$
diff --git a/resources/folderarchivesettings/CMakeLists.txt b/resources/folderarchivesettings/CMakeLists.txt
new file mode 100644 (file)
index 0000000..90d0baa
--- /dev/null
@@ -0,0 +1,32 @@
+project(folderarchivesettings)
+
+
+add_definitions(-DTRANSLATION_DOMAIN=\"libfolderarchivesettings\")
+
+
+set(folderarchivesettings_SRCS
+   folderarchivesettingpage.cpp
+   folderarchiveutil.cpp
+   folderarchiveaccountinfo.cpp 
+)
+
+add_library(folderarchivesettings  ${folderarchivesettings_SRCS} )
+generate_export_header(folderarchivesettings BASE_NAME folderarchivesettings)
+
+target_link_libraries(folderarchivesettings
+PRIVATE
+                      KF5::Mime
+                      KF5::AkonadiCore
+                      KF5::AkonadiWidgets
+                      KF5::ConfigCore
+                      KF5::I18n
+                      Qt5::DBus
+)
+
+set_target_properties(folderarchivesettings PROPERTIES VERSION ${KDEPIMRUNTIME_LIB_VERSION} SOVERSION ${KDEPIMRUNTIME_LIB_SOVERSION} )
+
+install(TARGETS folderarchivesettings ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP)
+if (BUILD_TESTING)
+   add_subdirectory(autotests)
+endif()
+
diff --git a/resources/folderarchivesettings/Messages.sh b/resources/folderarchivesettings/Messages.sh
new file mode 100755 (executable)
index 0000000..800a8b3
--- /dev/null
@@ -0,0 +1,2 @@
+#! /usr/bin/env bash
+$XGETTEXT *.cpp -o $podir/libfolderarchivesettings.pot
diff --git a/resources/folderarchivesettings/autotests/CMakeLists.txt b/resources/folderarchivesettings/autotests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..99ee61e
--- /dev/null
@@ -0,0 +1,13 @@
+
+# Convenience macro to add unit tests.
+macro( folderarchive_kmail _source )
+  set( _test ${_source} ../folderarchiveaccountinfo.cpp )
+  get_filename_component( _name ${_source} NAME_WE )
+  add_executable( ${_name} ${_test} )
+  add_test( ${_name} ${_name} )
+  ecm_mark_as_test(folderararchive-${_name})
+
+  target_link_libraries( ${_name} Qt5::Test Qt5::Core  KF5::AkonadiCore KF5::ConfigCore)
+endmacro()
+
+folderarchive_kmail(folderarchiveaccountinfotest.cpp)
diff --git a/resources/folderarchivesettings/autotests/folderarchiveaccountinfotest.cpp b/resources/folderarchivesettings/autotests/folderarchiveaccountinfotest.cpp
new file mode 100644 (file)
index 0000000..7da553d
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+  Copyright (c) 2014 Montel Laurent <montel@kde.org>
+
+  This program is free software; you can redistribute it and/or modify it
+  under the terms of the GNU General Public License, version 2, as
+  published by the Free Software Foundation.
+
+  This program is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#include "folderarchiveaccountinfotest.h"
+#include "../folderarchiveaccountinfo.h"
+#include <AkonadiCore/Collection>
+#include <qtest.h>
+#include <KSharedConfig>
+
+FolderArchiveAccountInfoTest::FolderArchiveAccountInfoTest(QObject *parent)
+    : QObject(parent)
+{
+
+}
+
+FolderArchiveAccountInfoTest::~FolderArchiveAccountInfoTest()
+{
+
+}
+
+void FolderArchiveAccountInfoTest::shouldHaveDefaultValue()
+{
+    FolderArchiveAccountInfo info;
+    QVERIFY(info.instanceName().isEmpty());
+    QCOMPARE(info.archiveTopLevel(), Akonadi::Collection(-1).id());
+    QCOMPARE(info.folderArchiveType(), FolderArchiveAccountInfo::UniqueFolder);
+    QCOMPARE(info.enabled(), false);
+    QCOMPARE(info.keepExistingStructure(), false);
+    QCOMPARE(info.isValid(), false);
+
+}
+
+void FolderArchiveAccountInfoTest::shouldBeValid()
+{
+    FolderArchiveAccountInfo info;
+    QVERIFY(!info.isValid());
+    info.setArchiveTopLevel(Akonadi::Collection(42).id());
+    QVERIFY(!info.isValid());
+    info.setInstanceName(QStringLiteral("FOO"));
+    QVERIFY(info.isValid());
+}
+
+void FolderArchiveAccountInfoTest::shouldRestoreFromSettings()
+{
+    FolderArchiveAccountInfo info;
+    info.setInstanceName(QStringLiteral("FOO1"));
+    info.setArchiveTopLevel(Akonadi::Collection(42).id());
+    info.setFolderArchiveType(FolderArchiveAccountInfo::FolderByMonths);
+    info.setEnabled(true);
+    info.setKeepExistingStructure(true);
+
+    KConfigGroup grp(KSharedConfig::openConfig(), "testsettings");
+    info.writeConfig(grp);
+
+    FolderArchiveAccountInfo restoreInfo(grp);
+    QCOMPARE(info, restoreInfo);
+}
+
+QTEST_MAIN(FolderArchiveAccountInfoTest)
diff --git a/resources/folderarchivesettings/autotests/folderarchiveaccountinfotest.h b/resources/folderarchivesettings/autotests/folderarchiveaccountinfotest.h
new file mode 100644 (file)
index 0000000..de393f3
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+  Copyright (c) 2014 Montel Laurent <montel@kde.org>
+
+  This program is free software; you can redistribute it and/or modify it
+  under the terms of the GNU General Public License, version 2, as
+  published by the Free Software Foundation.
+
+  This program is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#ifndef FOLDERARCHIVEACCOUNTINFOTEST_H
+#define FOLDERARCHIVEACCOUNTINFOTEST_H
+
+#include <QObject>
+
+class FolderArchiveAccountInfoTest : public QObject
+{
+    Q_OBJECT
+public:
+    explicit FolderArchiveAccountInfoTest(QObject *parent = Q_NULLPTR);
+    ~FolderArchiveAccountInfoTest();
+
+private Q_SLOTS:
+    void shouldHaveDefaultValue();
+    void shouldBeValid();
+    void shouldRestoreFromSettings();
+};
+
+#endif // FOLDERARCHIVEACCOUNTINFOTEST_H
+
diff --git a/resources/folderarchivesettings/folderarchiveaccountinfo.cpp b/resources/folderarchivesettings/folderarchiveaccountinfo.cpp
new file mode 100644 (file)
index 0000000..f3c0a70
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+  Copyright (c) 2013-2016 Montel Laurent <montel@kde.org>
+
+  This program is free software; you can redistribute it and/or modify it
+  under the terms of the GNU General Public License, version 2, as
+  published by the Free Software Foundation.
+
+  This program is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#include "folderarchiveaccountinfo.h"
+
+#include <KConfigGroup>
+
+FolderArchiveAccountInfo::FolderArchiveAccountInfo()
+    : mArchiveType(UniqueFolder),
+      mArchiveTopLevelCollectionId(-1),
+      mEnabled(false),
+      mKeepExistingStructure(false)
+{
+}
+
+FolderArchiveAccountInfo::FolderArchiveAccountInfo(const KConfigGroup &config)
+    : mArchiveType(UniqueFolder),
+      mArchiveTopLevelCollectionId(-1),
+      mEnabled(false),
+      mKeepExistingStructure(false)
+{
+    readConfig(config);
+}
+
+FolderArchiveAccountInfo::~FolderArchiveAccountInfo()
+{
+}
+
+bool FolderArchiveAccountInfo::isValid() const
+{
+    return (mArchiveTopLevelCollectionId > -1) && (!mInstanceName.isEmpty());
+}
+
+void FolderArchiveAccountInfo::setFolderArchiveType(FolderArchiveAccountInfo::FolderArchiveType type)
+{
+    mArchiveType = type;
+}
+
+FolderArchiveAccountInfo::FolderArchiveType FolderArchiveAccountInfo::folderArchiveType() const
+{
+    return mArchiveType;
+}
+
+void FolderArchiveAccountInfo::setArchiveTopLevel(Akonadi::Collection::Id id)
+{
+    mArchiveTopLevelCollectionId = id;
+}
+
+Akonadi::Collection::Id FolderArchiveAccountInfo::archiveTopLevel() const
+{
+    return mArchiveTopLevelCollectionId;
+}
+
+QString FolderArchiveAccountInfo::instanceName() const
+{
+    return mInstanceName;
+}
+
+void FolderArchiveAccountInfo::setInstanceName(const QString &instance)
+{
+    mInstanceName = instance;
+}
+
+void FolderArchiveAccountInfo::setEnabled(bool enabled)
+{
+    mEnabled = enabled;
+}
+
+bool FolderArchiveAccountInfo::enabled() const
+{
+    return mEnabled;
+}
+
+void FolderArchiveAccountInfo::setKeepExistingStructure(bool b)
+{
+    mKeepExistingStructure = b;
+}
+
+bool FolderArchiveAccountInfo::keepExistingStructure() const
+{
+    return mKeepExistingStructure;
+}
+
+void FolderArchiveAccountInfo::readConfig(const KConfigGroup &config)
+{
+    mInstanceName = config.readEntry(QStringLiteral("instanceName"));
+    mArchiveTopLevelCollectionId = config.readEntry(QStringLiteral("topLevelCollectionId"), -1);
+    mArchiveType = static_cast<FolderArchiveType>(config.readEntry("folderArchiveType", (int)UniqueFolder));
+    mEnabled = config.readEntry("enabled", false);
+    mKeepExistingStructure = config.readEntry("keepExistingStructure", false);
+}
+
+void FolderArchiveAccountInfo::writeConfig(KConfigGroup &config)
+{
+    config.writeEntry(QStringLiteral("instanceName"), mInstanceName);
+    if (mArchiveTopLevelCollectionId > -1) {
+        config.writeEntry(QStringLiteral("topLevelCollectionId"), mArchiveTopLevelCollectionId);
+    } else {
+        config.deleteEntry(QStringLiteral("topLevelCollectionId"));
+    }
+
+    config.writeEntry(QStringLiteral("folderArchiveType"), (int)mArchiveType);
+    config.writeEntry(QStringLiteral("enabled"), mEnabled);
+    config.writeEntry("keepExistingStructure", mKeepExistingStructure);
+}
+
+bool FolderArchiveAccountInfo::operator==(const FolderArchiveAccountInfo &other) const
+{
+    return (mInstanceName == other.instanceName())
+           && (mArchiveTopLevelCollectionId == other.archiveTopLevel())
+           && (mArchiveType == other.folderArchiveType())
+           && (mEnabled == other.enabled())
+           && (mKeepExistingStructure == other.keepExistingStructure());
+}
diff --git a/resources/folderarchivesettings/folderarchiveaccountinfo.h b/resources/folderarchivesettings/folderarchiveaccountinfo.h
new file mode 100644 (file)
index 0000000..baa551f
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+  Copyright (c) 2013-2016 Montel Laurent <montel@kde.org>
+
+  This program is free software; you can redistribute it and/or modify it
+  under the terms of the GNU General Public License, version 2, as
+  published by the Free Software Foundation.
+
+  This program is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#ifndef FOLDERARCHIVEACCOUNTINFO_H
+#define FOLDERARCHIVEACCOUNTINFO_H
+
+#include <KConfigGroup>
+#include <AkonadiCore/Collection>
+
+class FolderArchiveAccountInfo
+{
+public:
+    FolderArchiveAccountInfo();
+    FolderArchiveAccountInfo(const KConfigGroup &config);
+    ~FolderArchiveAccountInfo();
+
+    enum FolderArchiveType {
+        UniqueFolder,
+        FolderByMonths,
+        FolderByYears
+    };
+
+    bool isValid() const;
+
+    QString instanceName() const;
+    void setInstanceName(const QString &instance);
+
+    void setArchiveTopLevel(Akonadi::Collection::Id id);
+    Akonadi::Collection::Id archiveTopLevel() const;
+
+    void setFolderArchiveType(FolderArchiveType type);
+    FolderArchiveType folderArchiveType() const;
+
+    void setEnabled(bool enabled);
+    bool enabled() const;
+
+    void setKeepExistingStructure(bool b);
+    bool keepExistingStructure() const;
+
+    void writeConfig(KConfigGroup &config);
+    void readConfig(const KConfigGroup &config);
+
+    bool operator==(const FolderArchiveAccountInfo &other) const;
+
+private:
+    FolderArchiveAccountInfo::FolderArchiveType mArchiveType;
+    Akonadi::Collection::Id mArchiveTopLevelCollectionId;
+    QString mInstanceName;
+    bool mEnabled;
+    bool mKeepExistingStructure;
+};
+
+#endif // FOLDERARCHIVEACCOUNTINFO_H
diff --git a/resources/folderarchivesettings/folderarchivesettingpage.cpp b/resources/folderarchivesettings/folderarchivesettingpage.cpp
new file mode 100644 (file)
index 0000000..aab1967
--- /dev/null
@@ -0,0 +1,159 @@
+/*
+  Copyright (c) 2013-2016 Montel Laurent <montel@kde.org>
+
+  This program is free software; you can redistribute it and/or modify it
+  under the terms of the GNU General Public License, version 2, as
+  published by the Free Software Foundation.
+
+  This program is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#include "folderarchivesettingpage.h"
+#include "folderarchiveaccountinfo.h"
+#include "folderarchiveutil.h"
+
+#include <CollectionRequester>
+
+#include <kmime/kmime_message.h>
+
+#include <KLocalizedString>
+#include <KSharedConfig>
+
+#include <QCheckBox>
+#include <QVBoxLayout>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QComboBox>
+#include <QDBusReply>
+#include <QDBusInterface>
+#include <QDBusConnectionInterface>
+
+FolderArchiveComboBox::FolderArchiveComboBox(QWidget *parent)
+    : QComboBox(parent)
+{
+    initialize();
+}
+
+FolderArchiveComboBox::~FolderArchiveComboBox()
+{
+}
+
+void FolderArchiveComboBox::initialize()
+{
+    addItem(i18nc("@item:inlistbox for option \"Archive folder name\"", "Unique"),
+            FolderArchiveAccountInfo::UniqueFolder);
+    addItem(i18nc("@item:inlistbox for option \"Archive folder name\"", "Month and year"),
+            FolderArchiveAccountInfo::FolderByMonths);
+    addItem(i18nc("@item:inlistbox for option \"Archive folder name\"", "Year"),
+            FolderArchiveAccountInfo::FolderByYears);
+}
+
+void FolderArchiveComboBox::setType(FolderArchiveAccountInfo::FolderArchiveType type)
+{
+    const int index = findData(static_cast<int>(type));
+    if (index != -1) {
+        setCurrentIndex(index);
+    } else {
+        setCurrentIndex(0);
+    }
+}
+
+FolderArchiveAccountInfo::FolderArchiveType FolderArchiveComboBox::type() const
+{
+    return static_cast<FolderArchiveAccountInfo::FolderArchiveType>(itemData(currentIndex()).toInt());
+}
+
+FolderArchiveSettingPage::FolderArchiveSettingPage(const QString &instanceName, QWidget *parent)
+    : QWidget(parent),
+      mInstanceName(instanceName),
+      mInfo(Q_NULLPTR)
+{
+    QVBoxLayout *lay = new QVBoxLayout;
+    mEnabled = new QCheckBox(i18n("Enable"));
+    connect(mEnabled, &QCheckBox::toggled, this, &FolderArchiveSettingPage::slotEnableChanged);
+    lay->addWidget(mEnabled);
+
+    QHBoxLayout *hbox = new QHBoxLayout;
+    QLabel *lab = new QLabel(i18nc(
+                                 "@label:chooser for the folder that messages will be archived under",
+                                 "Archive into:"));
+    hbox->addWidget(lab);
+    mArchiveFolder = new Akonadi::CollectionRequester;
+    mArchiveFolder->setMimeTypeFilter(QStringList() << KMime::Message::mimeType());
+    hbox->addWidget(mArchiveFolder);
+    lay->addLayout(hbox);
+
+    hbox = new QHBoxLayout;
+    lab = new QLabel(i18nc("@label:listbox", "Archive folder name:"));
+    hbox->addWidget(lab);
+    mArchiveNamed = new FolderArchiveComboBox;
+    hbox->addWidget(mArchiveNamed);
+
+    lay->addLayout(hbox);
+
+    lay->addStretch();
+
+    setLayout(lay);
+}
+
+FolderArchiveSettingPage::~FolderArchiveSettingPage()
+{
+    delete mInfo;
+}
+
+void FolderArchiveSettingPage::slotEnableChanged(bool enabled)
+{
+    mArchiveFolder->setEnabled(enabled);
+    mArchiveNamed->setEnabled(enabled);
+}
+
+void FolderArchiveSettingPage::loadSettings()
+{
+    KConfig config(FolderArchive::FolderArchiveUtil::configFileName());
+    const QString groupName = FolderArchive::FolderArchiveUtil::groupConfigPattern() + mInstanceName;
+    if (config.hasGroup(groupName)) {
+        KConfigGroup grp = config.group(groupName);
+        mInfo = new FolderArchiveAccountInfo(grp);
+        mEnabled->setChecked(mInfo->enabled());
+        mArchiveFolder->setCollection(Akonadi::Collection(mInfo->archiveTopLevel()));
+        mArchiveNamed->setType(mInfo->folderArchiveType());
+    } else {
+        mInfo = new FolderArchiveAccountInfo();
+        mEnabled->setChecked(false);
+    }
+    slotEnableChanged(mEnabled->isChecked());
+}
+
+void FolderArchiveSettingPage::writeSettings()
+{
+    KConfig config(FolderArchive::FolderArchiveUtil::configFileName());
+    KConfigGroup grp = config.group(FolderArchive::FolderArchiveUtil::groupConfigPattern() + mInstanceName);
+    mInfo->setInstanceName(mInstanceName);
+    if (mArchiveFolder->collection().isValid()) {
+        mInfo->setEnabled(mEnabled->isChecked());
+        mInfo->setArchiveTopLevel(mArchiveFolder->collection().id());
+    } else {
+        mInfo->setEnabled(false);
+        mInfo->setArchiveTopLevel(-1);
+    }
+
+    mInfo->setFolderArchiveType(mArchiveNamed->type());
+    mInfo->writeConfig(grp);
+
+    //Update cache from KMail
+    const QString kmailInterface = QStringLiteral("org.kde.kmail");
+    QDBusReply<bool> reply = QDBusConnection::sessionBus().interface()->isServiceRegistered(kmailInterface);
+    if (!reply.isValid() || !reply.value()) {
+        return;
+    }
+    QDBusInterface kmail(kmailInterface, QStringLiteral("/KMail"), QStringLiteral("org.kde.kmail.kmail"));
+    kmail.asyncCall(QStringLiteral("reloadFolderArchiveConfig"));
+}
+
diff --git a/resources/folderarchivesettings/folderarchivesettingpage.h b/resources/folderarchivesettings/folderarchivesettingpage.h
new file mode 100644 (file)
index 0000000..26661ee
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+  Copyright (c) 2013-2016 Montel Laurent <montel@kde.org>
+
+  This program is free software; you can redistribute it and/or modify it
+  under the terms of the GNU General Public License, version 2, as
+  published by the Free Software Foundation.
+
+  This program is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#ifndef FOLDERARCHIVESETTINGPAGE_H
+#define FOLDERARCHIVESETTINGPAGE_H
+
+#include "folderarchiveaccountinfo.h"
+#include "folderarchivesettings_export.h"
+#include <QWidget>
+#include <QComboBox>
+
+class QCheckBox;
+namespace Akonadi
+{
+class CollectionRequester;
+}
+
+class FolderArchiveComboBox : public QComboBox
+{
+    Q_OBJECT
+public:
+    explicit FolderArchiveComboBox(QWidget *parent = Q_NULLPTR);
+    ~FolderArchiveComboBox();
+
+    void setType(FolderArchiveAccountInfo::FolderArchiveType type);
+    FolderArchiveAccountInfo::FolderArchiveType type() const;
+
+private:
+    void initialize();
+};
+
+class FolderArchiveAccountInfo;
+class FOLDERARCHIVESETTINGS_EXPORT FolderArchiveSettingPage : public QWidget
+{
+    Q_OBJECT
+public:
+    explicit FolderArchiveSettingPage(const QString &instanceName, QWidget *parent = Q_NULLPTR);
+    ~FolderArchiveSettingPage();
+
+    void loadSettings();
+    void writeSettings();
+
+private Q_SLOTS:
+    void slotEnableChanged(bool enabled);
+
+private:
+    QString mInstanceName;
+    QCheckBox *mEnabled;
+    FolderArchiveComboBox *mArchiveNamed;
+    Akonadi::CollectionRequester *mArchiveFolder;
+    FolderArchiveAccountInfo *mInfo;
+};
+
+#endif // FOLDERARCHIVESETTINGPAGE_H
diff --git a/resources/folderarchivesettings/folderarchiveutil.cpp b/resources/folderarchivesettings/folderarchiveutil.cpp
new file mode 100644 (file)
index 0000000..654495c
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+  Copyright (c) 2013-2016 Montel Laurent <montel@kde.org>
+
+  This program is free software; you can redistribute it and/or modify it
+  under the terms of the GNU General Public License, version 2, as
+  published by the Free Software Foundation.
+
+  This program is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#include "folderarchiveutil.h"
+
+using namespace FolderArchive;
+
+QString FolderArchiveUtil::groupConfigPattern()
+{
+    return QStringLiteral("FolderArchiveAccount ");
+}
+
+QString FolderArchiveUtil::configFileName()
+{
+    return QStringLiteral("foldermailarchiverc");
+}
diff --git a/resources/folderarchivesettings/folderarchiveutil.h b/resources/folderarchivesettings/folderarchiveutil.h
new file mode 100644 (file)
index 0000000..da1d34b
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+  Copyright (c) 2013-2016 Montel Laurent <montel@kde.org>
+
+  This program is free software; you can redistribute it and/or modify it
+  under the terms of the GNU General Public License, version 2, as
+  published by the Free Software Foundation.
+
+  This program is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#ifndef FOLDERARCHIVEUTIL_H
+#define FOLDERARCHIVEUTIL_H
+
+#include <QString>
+namespace FolderArchive
+{
+namespace FolderArchiveUtil
+{
+QString groupConfigPattern();
+QString configFileName();
+}
+}
+
+#endif // FOLDERARCHIVEUTIL_H
diff --git a/resources/gmail/CMakeLists.txt b/resources/gmail/CMakeLists.txt
new file mode 100644 (file)
index 0000000..41b1dbf
--- /dev/null
@@ -0,0 +1,51 @@
+# REACTIVATE IT
+#add_subdirectory(saslplugin)
+add_definitions( -DQT_NO_CAST_FROM_ASCII )
+add_definitions( -DQT_NO_CAST_TO_ASCII )
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_gmail_resource\")
+
+
+include_directories(
+    ${CMAKE_CURRENT_SOURCE_DIR}/..
+    ${CMAKE_CURRENT_SOURCE_DIR}/../imap
+    ${CMAKE_CURRENT_BINARY_DIR}/../imap
+    ${CMAKE_CURRENT_SOURCE_DIR}/../folderarchivesettings/
+)
+
+
+
+########### next target ###############
+
+set(gmailresource_SRCS
+    gmailresource.cpp
+    gmailretrievecollectionstask.cpp
+    gmailretrieveitemstask.cpp
+    gmailmessagehelper.cpp
+    gmailpasswordrequester.cpp
+    gmailresourcestate.cpp
+    gmailconfigdialog.cpp
+    gmailsettings.cpp
+    gmaillinkitemstask.cpp
+    gmaillabelattribute.cpp
+    gmailchangeitemslabelstask.cpp
+)
+
+ki18n_wrap_ui(gmailresource_SRCS gmailconfigdialog.ui)
+#kconfig_add_kcfg_files(gmailresource_SRCS settingsbase.kcfgc)
+
+add_executable(akonadi_gmail_resource ${gmailresource_SRCS})
+target_link_libraries(akonadi_gmail_resource
+    KF5::AkonadiCore
+    KF5::IMAP
+    KF5::MailTransport
+    KF5::KIOCore
+    KF5::Mime
+    KF5::AkonadiMime
+    KF5::IdentityManagement
+    KF5::GAPICore
+    imapresource
+    folderarchivesettings
+)
+
+install(FILES gmailresource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents")
+install(TARGETS akonadi_gmail_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
diff --git a/resources/gmail/Messages.sh b/resources/gmail/Messages.sh
new file mode 100755 (executable)
index 0000000..780159e
--- /dev/null
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+$EXTRACTRC *.ui *.kcfg >> rc.cpp
+$XGETTEXT *.cpp -o $podir/akonadi_gmail_resource.pot
diff --git a/resources/gmail/gmailchangeitemslabelstask.cpp b/resources/gmail/gmailchangeitemslabelstask.cpp
new file mode 100644 (file)
index 0000000..b47e567
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#include "gmailchangeitemslabelstask.h"
+
+#include <KImap/Session>
+#include <KImap/StoreJob>
+#include <KImap/SelectJob>
+
+GmailChangeItemsLabelsTask::GmailChangeItemsLabelsTask(ResourceStateInterface::Ptr resource, QObject *parent)
+    : ChangeItemsFlagsTask(resource, parent)
+{
+}
+
+GmailChangeItemsLabelsTask::~GmailChangeItemsLabelsTask()
+{
+}
+
+void GmailChangeItemsLabelsTask::doStart(KIMAP::Session *session)
+{
+    const QString mailbox = QLatin1String("[Gmail]/All Mail");
+    if (session->selectedMailBox() != mailbox) {
+        KIMAP::SelectJob *select = new KIMAP::SelectJob(session);
+        select->setMailBox(mailbox);
+        connect(select, SIGNAL(finished(KJob*)),
+                this, SLOT(onSelectDone(KJob*)));
+        select->start();
+    } else {
+        if (!addedFlags().isEmpty()) {
+            triggerAppendFlagsJob(session);
+        } else if (!removedFlags().isEmpty()) {
+            triggerRemoveFlagsJob(session);
+        } else {
+            changeProcessed();
+        }
+    }
+}
+
+void GmailChangeItemsLabelsTask::triggerAppendFlagsJob(KIMAP::Session *session)
+{
+    KIMAP::StoreJob *store = prepareJob(session);
+    store->setGMLabels(addedFlags().toList());
+    store->setMode(KIMAP::StoreJob::AppendFlags);
+    connect(store, SIGNAL(result(KJob*)),
+            this, SLOT(onAppendFlagsDone(KJob*)));
+    store->start();
+}
+
+void GmailChangeItemsLabelsTask::triggerRemoveFlagsJob(KIMAP::Session *session)
+{
+    KIMAP::StoreJob *store = prepareJob(session);
+    store->setGMLabels(removedFlags().toList());
+    store->setMode(KIMAP::StoreJob::RemoveFlags);
+    connect(store, SIGNAL(result(KJob*)),
+            this, SLOT(onRemoveFlagsDone(KJob*)));
+    store->start();
+}
diff --git a/resources/gmail/gmailchangeitemslabelstask.h b/resources/gmail/gmailchangeitemslabelstask.h
new file mode 100644 (file)
index 0000000..0bc9bf5
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifndef GMAILCHANGEGMLABELTASK_H
+#define GMAILCHANGEGMLABELTASK_H
+
+#include <imap/changeitemsflagstask.h>
+
+class GmailChangeItemsLabelsTask : public ChangeItemsFlagsTask
+{
+    Q_OBJECT
+public:
+    explicit GmailChangeItemsLabelsTask(ResourceStateInterface::Ptr resource, QObject *parent = 0);
+    virtual ~GmailChangeItemsLabelsTask();
+
+protected:
+    void doStart(KIMAP::Session *session);
+
+protected:
+    void triggerAppendFlagsJob(KIMAP::Session *session);
+    void triggerRemoveFlagsJob(KIMAP::Session *session);
+};
+
+#endif // GMAILCHANGEGMLABELTASK_H
diff --git a/resources/gmail/gmailconfigdialog.cpp b/resources/gmail/gmailconfigdialog.cpp
new file mode 100644 (file)
index 0000000..4f08823
--- /dev/null
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#include "gmailconfigdialog.h"
+#include "gmailsettings.h"
+#include "gmailresource.h"
+#include "ui_gmailconfigdialog.h"
+#include <folderarchivesettingpage.h>
+
+#include <mailtransport/transport.h>
+
+#include <kidentitymanagement/identitymanager.h>
+#include <kidentitymanagement/identitycombo.h>
+
+#include <AkonadiAgentBase/resourcesettings.h>
+
+#include <kgapi/account.h>
+#include <kgapi/authjob.h>
+
+#include <imap/imapaccount.h>
+#include <imap/subscriptiondialog.h>
+
+#include <KLocalizedString>
+#include <KMessageBox>
+
+#include <QPointer>
+#include <QVBoxLayout>
+#include <QDialogButtonBox>
+
+GmailConfigDialog::GmailConfigDialog(GmailResource *resource, WId parent)
+    : QDialog()
+    , m_parentResource(resource)
+    , m_ui(new Ui::GmailConfigDialog)
+    , m_subscriptionsChanged(false)
+    , m_shouldClearCache(false)
+{
+    m_parentResource->settings()->setWinId(parent);
+
+    QWidget *mainWidget = new QWidget(this);
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    setLayout(mainLayout);
+    mainLayout->addWidget(mainWidget);
+    m_ui->setupUi(mainWidget);
+    m_folderArchiveSettingPage = new FolderArchiveSettingPage(resource->identifier());
+    m_ui->tabWidget->addTab(m_folderArchiveSettingPage, i18n("Archive Folder"));
+
+    m_ui->checkInterval->setSuffix(ki18np(" minute", " minutes"));
+    m_ui->checkInterval->setRange(Akonadi::ResourceSettings::self()->minimumCheckInterval(), 10000);
+    m_ui->checkInterval->setSingleStep(1);
+
+    m_identityManager = new KIdentityManagement::IdentityManager(false, this, "mIdentityManager");
+    m_identityCombobox = new KIdentityManagement::IdentityCombo(m_identityManager, this);
+    m_ui->identityLabel->setBuddy(m_identityCombobox);
+    m_ui->identityLayout->addWidget(m_identityCombobox, 1);
+    m_ui->identityLabel->setBuddy(m_identityCombobox);
+
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    mOkButton = buttonBox->button(QDialogButtonBox::Ok);
+    mOkButton->setDefault(true);
+    mOkButton->setShortcut(Qt::CTRL | Qt::Key_Return);
+    connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
+    connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
+    mainLayout->addWidget(buttonBox);
+
+    connect(m_ui->subscriptionEnabled, SIGNAL(toggled(bool)),
+            this, SLOT(slotSubcriptionCheckboxChanged()));
+    connect(m_ui->subscriptionButton, SIGNAL(clicked(bool)),
+            this, SLOT(slotManageSubscriptions()));
+
+    connect(m_ui->authenticateButton, SIGNAL(clicked(bool)),
+            this, SLOT(slotAuthenticate()));
+    connect(m_ui->changeAuthButton, SIGNAL(clicked(bool)),
+            this, SLOT(slotAuthenticate()));
+
+    connect(m_ui->useDefaultIdentityCheck, SIGNAL(toggled(bool)),
+            this, SLOT(slotIdentityCheckboxChanged()));
+    connect(m_ui->enableMailCheckBox, SIGNAL(toggled(bool)),
+            this, SLOT(slotMailCheckboxChanged()));
+
+    connect(m_parentResource->settings(), SIGNAL(accountRequestCompleted(KGAPI2::AccountPtr,bool)),
+            this, SLOT(onAccountRequestCompleted(KGAPI2::AccountPtr,bool)));
+
+    readSettings();
+    slotComplete();
+    slotSubcriptionCheckboxChanged();
+    slotIdentityCheckboxChanged();
+
+    //connect(this, SIGNAL(applyClicked()),
+    //this, SLOT(applySettings()) );
+    connect(mOkButton, SIGNAL(clicked()),
+            this, SLOT(applySettings()));
+}
+
+GmailConfigDialog::~GmailConfigDialog()
+{
+    delete m_ui;
+}
+
+bool GmailConfigDialog::shouldClearCache() const
+{
+    return m_shouldClearCache;
+}
+
+void GmailConfigDialog::slotSubcriptionCheckboxChanged()
+{
+    m_ui->subscriptionButton->setEnabled(m_ui->subscriptionEnabled->isChecked());
+}
+
+void GmailConfigDialog::slotIdentityCheckboxChanged()
+{
+    m_identityCombobox->setEnabled(!m_ui->useDefaultIdentityCheck->isChecked());
+}
+
+void GmailConfigDialog::slotMailCheckboxChanged()
+{
+    m_ui->checkInterval->setEnabled(m_ui->enableMailCheckBox->isChecked());
+}
+
+void GmailConfigDialog::applySettings()
+{
+    m_folderArchiveSettingPage->writeSettings();
+    m_parentResource->setName(m_ui->usernameLabel->text());
+
+    GmailSettings *settings = static_cast<GmailSettings *>(m_parentResource->settings());
+    settings->setImapServer(QLatin1String("imap.gmail.com"));
+    settings->setImapPort(993);
+    settings->setUserName(m_account->accountName());
+    settings->setPassword(m_account->accessToken());
+    settings->setRefreshToken(m_account->refreshToken());
+    settings->setSafety(QLatin1String("SSL"));
+    settings->setSubscriptionEnabled(m_ui->subscriptionEnabled->isChecked());
+    settings->setIntervalCheckTime(m_ui->checkInterval->value());
+    settings->setDisconnectedModeEnabled(m_ui->disconnectedModeEnabled->isChecked());
+
+    /* Gmail does not support sieve */
+    settings->setSieveSupport(false);
+
+    settings->setAutomaticExpungeEnabled(m_ui->autoExpungeCheck->isChecked());
+    settings->setUseDefaultIdentity(m_ui->useDefaultIdentityCheck->isChecked());
+    if (!m_ui->useDefaultIdentityCheck->isChecked()) {
+        settings->setAccountIdentity(m_identityCombobox->currentIdentity());
+    }
+
+    settings->setIntervalCheckEnabled(m_ui->enableMailCheckBox->isChecked());
+    if (m_ui->enableMailCheckBox->isChecked()) {
+        settings->setIntervalCheckTime(m_ui->checkInterval->value());
+    }
+
+    settings->save();
+
+    if (m_oldResourceName != m_account->accountName() && !m_account->accountName().isEmpty()) {
+        settings->renameRootCollection(m_account->accountName());
+    }
+}
+
+void GmailConfigDialog::readSettings()
+{
+    m_folderArchiveSettingPage->loadSettings();
+    m_ui->usernameLabel->setText(m_parentResource->name());
+    m_oldResourceName = m_parentResource->name();
+
+    GmailSettings *settings = static_cast<GmailSettings *>(m_parentResource->settings());
+
+    m_account = KGAPI2::AccountPtr(new KGAPI2::Account);
+    if (!m_parentResource->name().startsWith(m_parentResource->defaultName())) {
+        m_account->setAccountName(m_parentResource->name());
+        m_ui->usernameLabel->setText(m_account->accountName());
+        m_ui->authenticateButton->setVisible(false);
+        m_ui->currentAccountBox->setVisible(true);
+
+        bool rejected = false;
+        const QString accessToken = settings->password(&rejected);
+        const QString refreshToken = settings->refreshToken(&rejected);
+        if (rejected) {
+            //m_ui->password->setEnabled( false );
+            KMessageBox::information(0, i18n("Could not access KWallet. If you want to use Gmail resource, you have to activate it."));
+        } else {
+            m_account->setAccessToken(accessToken);
+            m_account->setRefreshToken(refreshToken);
+        }
+    } else {
+        m_ui->currentAccountBox->setVisible(false);
+        m_ui->authenticateButton->setVisible(true);
+    }
+
+    m_ui->subscriptionEnabled->setChecked(settings->subscriptionEnabled());
+
+    m_ui->enableMailCheckBox->setChecked(settings->intervalCheckEnabled());
+    m_ui->checkInterval->setEnabled(m_ui->enableMailCheckBox->isChecked());
+    m_ui->checkInterval->setValue(settings->intervalCheckTime());
+    m_ui->disconnectedModeEnabled->setChecked(settings->disconnectedModeEnabled());
+
+    m_ui->useDefaultIdentityCheck->setChecked(settings->useDefaultIdentity());
+    if (!m_ui->useDefaultIdentityCheck->isChecked()) {
+        m_identityCombobox->setCurrentIdentity(settings->accountIdentity());
+    }
+
+    m_ui->autoExpungeCheck->setChecked(settings->automaticExpungeEnabled());
+}
+
+void GmailConfigDialog::slotComplete()
+{
+    const bool ok = (m_account || !m_account->accountName().isEmpty());
+    mOkButton->setEnabled(ok);
+}
+
+void GmailConfigDialog::slotManageSubscriptions()
+{
+    ImapAccount account;
+
+    account.setServer(QLatin1String("imap.gmail.com"));
+    account.setPort(993);
+    account.setUserName(m_account->accountName());
+    account.setSubscriptionEnabled(m_ui->subscriptionEnabled->isChecked());
+
+    account.setEncryptionMode(KIMAP::LoginJob::SslV3);
+    account.setAuthenticationMode(KIMAP::LoginJob::XOAuth2);
+
+    QPointer<SubscriptionDialog> subscriptions = new SubscriptionDialog(this);
+    subscriptions->setWindowTitle(i18n("Serverside Subscription"));
+    subscriptions->setWindowIcon(QIcon::fromTheme(QLatin1String("network-server")));
+    subscriptions->connectAccount(account, m_account->accessToken());
+    m_subscriptionsChanged = subscriptions->isSubscriptionChanged();
+
+    subscriptions->exec();
+    delete subscriptions;
+
+    m_ui->subscriptionEnabled->setChecked(account.isSubscriptionEnabled());
+}
+
+void GmailConfigDialog::slotAuthenticate()
+{
+    GmailSettings *settings = static_cast<GmailSettings *>(m_parentResource->settings());
+    settings->clearCachedPassword();
+    settings->storeAccount(KGAPI2::AccountPtr());
+    settings->requestAccount(true);
+    m_shouldClearCache = true;
+}
+
+void GmailConfigDialog::onAccountRequestCompleted(const KGAPI2::AccountPtr &account, bool userRejected)
+{
+    if (userRejected || account.isNull()) {
+        m_account = KGAPI2::AccountPtr();
+
+        m_ui->currentAccountBox->setVisible(false);
+        m_ui->authenticateButton->setVisible(true);
+    } else {
+        m_account = account;
+        m_ui->currentAccountBox->setVisible(true);
+        m_ui->usernameLabel->setText(m_account->accountName());
+        m_ui->authenticateButton->setVisible(false);
+    }
+
+    GmailSettings *settings = static_cast<GmailSettings *>(m_parentResource->settings());
+    settings->storeAccount(m_account);
+    slotComplete();
+}
+
diff --git a/resources/gmail/gmailconfigdialog.h b/resources/gmail/gmailconfigdialog.h
new file mode 100644 (file)
index 0000000..628ab85
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifndef GMAILSETUPSERVER_H
+#define GMAILSETUPSERVER_H
+
+#include <QDialog>
+
+#include <KGAPI/Account>
+
+namespace Ui
+{
+class GmailConfigDialog;
+}
+
+namespace KIdentityManagement
+{
+class IdentityCombo;
+class IdentityManager;
+}
+class QPushButton;
+class GmailResource;
+class FolderArchiveSettingPage;
+class GmailConfigDialog : public QDialog
+{
+    Q_OBJECT
+
+public:
+    explicit GmailConfigDialog(GmailResource *resource, WId parent);
+    virtual ~GmailConfigDialog();
+
+    bool shouldClearCache() const;
+
+private Q_SLOTS:
+    /**
+     * Call this if you want the settings saved from this page.
+     */
+    void applySettings();
+    void slotIdentityCheckboxChanged();
+    void slotMailCheckboxChanged();
+    void slotManageSubscriptions();
+    void slotSubcriptionCheckboxChanged();
+    void slotComplete();
+
+    void slotAuthenticate();
+    void onAccountRequestCompleted(const KGAPI2::AccountPtr &account, bool userRejected);
+
+private:
+    void readSettings();
+
+    GmailResource *m_parentResource;
+    Ui::GmailConfigDialog *m_ui;
+    bool m_subscriptionsChanged;
+    bool m_shouldClearCache;
+    KIdentityManagement::IdentityManager *m_identityManager;
+    KIdentityManagement::IdentityCombo *m_identityCombobox;
+    QString m_oldResourceName;
+    KGAPI2::AccountPtr m_account;
+    FolderArchiveSettingPage *m_folderArchiveSettingPage;
+    QPushButton *mOkButton;
+};
+
+#endif // GMAILSETUPSERVER_H
diff --git a/resources/gmail/gmailconfigdialog.ui b/resources/gmail/gmailconfigdialog.ui
new file mode 100644 (file)
index 0000000..f948e45
--- /dev/null
@@ -0,0 +1,232 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>GmailConfigDialog</class>
+ <widget class="QWidget" name="GmailConfigDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>449</width>
+    <height>460</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QTabWidget" name="tabWidget">
+     <property name="currentIndex">
+      <number>0</number>
+     </property>
+     <widget class="QWidget" name="generalTab">
+      <attribute name="title">
+       <string>General</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_3">
+       <item>
+        <layout class="QVBoxLayout" name="accountBox">
+         <item>
+          <widget class="QGroupBox" name="currentAccountBox">
+           <property name="title">
+            <string>Account</string>
+           </property>
+           <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1,0">
+            <item>
+             <widget class="QLabel" name="label_1">
+              <property name="text">
+               <string>Authenticated as:</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QLabel" name="usernameLabel">
+              <property name="font">
+               <font>
+                <weight>75</weight>
+                <bold>true</bold>
+               </font>
+              </property>
+              <property name="text">
+               <string/>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QPushButton" name="changeAuthButton">
+              <property name="text">
+               <string>Change...</string>
+              </property>
+             </widget>
+            </item>
+           </layout>
+          </widget>
+         </item>
+        </layout>
+       </item>
+       <item>
+        <widget class="QPushButton" name="authenticateButton">
+         <property name="text">
+          <string>Authenticate</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QGroupBox" name="mailCheckBox">
+         <property name="title">
+          <string>Mail Checking Options</string>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_2">
+          <item>
+           <widget class="QCheckBox" name="disconnectedModeEnabled">
+            <property name="text">
+             <string>&amp;Download all messages for offline use</string>
+            </property>
+            <property name="checked">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QCheckBox" name="enableMailCheckBox">
+            <property name="text">
+             <string>Enable &amp;interval mail checking</string>
+            </property>
+            <property name="checked">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <layout class="QHBoxLayout" name="horizontalLayout_2">
+            <item>
+             <widget class="QLabel" name="label_2">
+              <property name="text">
+               <string>Check mail interval:</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="KPluralHandlingSpinBox" name="checkInterval">
+              <property name="value">
+               <number>15</number>
+              </property>
+              <property name="minimum">
+               <number>5</number>
+              </property>
+             </widget>
+            </item>
+           </layout>
+          </item>
+          <item>
+           <spacer name="verticalSpacer">
+            <property name="orientation">
+             <enum>Qt::Vertical</enum>
+            </property>
+            <property name="sizeHint" stdset="0">
+             <size>
+              <width>20</width>
+              <height>40</height>
+             </size>
+            </property>
+           </spacer>
+          </item>
+         </layout>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="advancedTab">
+      <attribute name="title">
+       <string>Advanced</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_4">
+       <item>
+        <widget class="QGroupBox" name="groupBox_3">
+         <property name="title">
+          <string>IMAP Settings</string>
+         </property>
+         <layout class="QGridLayout" name="gridLayout">
+          <item row="0" column="0" colspan="2">
+           <widget class="QCheckBox" name="subscriptionEnabled">
+            <property name="text">
+             <string>&amp;Synchronize only selected folders</string>
+            </property>
+           </widget>
+          </item>
+          <item row="1" column="1">
+           <widget class="QPushButton" name="subscriptionButton">
+            <property name="text">
+             <string>Select folders to synchronize...</string>
+            </property>
+           </widget>
+          </item>
+          <item row="2" column="0" colspan="2">
+           <widget class="QCheckBox" name="autoExpungeCheck">
+            <property name="text">
+             <string>Automatical&amp;ly compact folders (expunge deleted messages)</string>
+            </property>
+            <property name="checked">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <widget class="QGroupBox" name="identityBox">
+         <property name="title">
+          <string>Identity Settings</string>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_5">
+          <item>
+           <widget class="QCheckBox" name="useDefaultIdentityCheck">
+            <property name="text">
+             <string>Use &amp;default identity</string>
+            </property>
+            <property name="checked">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <layout class="QHBoxLayout" name="identityLayout">
+            <item>
+             <widget class="QLabel" name="identityLabel">
+              <property name="text">
+               <string>Identity:</string>
+              </property>
+             </widget>
+            </item>
+           </layout>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <spacer name="verticalSpacer_2">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>40</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KPluralHandlingSpinBox</class>
+   <extends>QSpinBox</extends>
+   <header>kpluralhandlingspinbox.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/resources/gmail/gmaillabelattribute.cpp b/resources/gmail/gmaillabelattribute.cpp
new file mode 100644 (file)
index 0000000..7a3c05f
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#include "gmaillabelattribute.h"
+
+GmailLabelAttribute::GmailLabelAttribute()
+    : Akonadi::Attribute()
+{
+}
+
+GmailLabelAttribute::GmailLabelAttribute(const QByteArray &label)
+    : Akonadi::Attribute()
+    , mLabel(label)
+{
+}
+
+GmailLabelAttribute::~GmailLabelAttribute()
+{
+}
+
+QByteArray GmailLabelAttribute::label() const
+{
+    return mLabel;
+}
+
+void GmailLabelAttribute::setLabel(const QByteArray &label)
+{
+    mLabel = label;
+}
+
+void GmailLabelAttribute::deserialize(const QByteArray &data)
+{
+    mLabel = data;
+}
+
+QByteArray GmailLabelAttribute::serialized() const
+{
+    return mLabel;
+}
+
+Akonadi::Attribute *GmailLabelAttribute::clone() const
+{
+    return new GmailLabelAttribute(mLabel);
+}
+
+QByteArray GmailLabelAttribute::type() const
+{
+    return "GmailLabel";
+}
+
+bool GmailLabelAttribute::isAllMail() const
+{
+    return mLabel == "\\All";
+}
+
+bool GmailLabelAttribute::isDrafts() const
+{
+    return mLabel == "\\Draft";
+}
+
+bool GmailLabelAttribute::isFlagged() const
+{
+    return mLabel == "\\Flagged";
+}
+
+bool GmailLabelAttribute::isImportant() const
+{
+    return mLabel == "\\Important";
+}
+
+bool GmailLabelAttribute::isInbox() const
+{
+    return mLabel == "\\Inbox";
+}
+
+bool GmailLabelAttribute::isJunk() const
+{
+    return mLabel == "\\Junk";
+}
+
+bool GmailLabelAttribute::isSent() const
+{
+    return mLabel == "\\Sent";
+}
+
+bool GmailLabelAttribute::isTrash() const
+{
+    return mLabel == "\\Trash";
+}
diff --git a/resources/gmail/gmaillabelattribute.h b/resources/gmail/gmaillabelattribute.h
new file mode 100644 (file)
index 0000000..2efbd76
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifndef GMAILLABELATTRIBUTE_H
+#define GMAILLABELATTRIBUTE_H
+
+#include <AkonadiCore/Attribute>
+#include <QByteArray>
+
+class GmailLabelAttribute : public Akonadi::Attribute
+{
+public:
+    explicit GmailLabelAttribute();
+    explicit GmailLabelAttribute(const QByteArray &label);
+    virtual ~GmailLabelAttribute();
+
+    QByteArray label() const;
+    void setLabel(const QByteArray &label);
+
+    bool isDrafts() const;
+    bool isTrash() const;
+    bool isImportant() const;
+    bool isSent() const;
+    bool isJunk() const;
+    bool isFlagged() const;
+    bool isInbox() const;
+    bool isAllMail() const;
+
+    void deserialize(const QByteArray &data);
+    QByteArray serialized() const;
+    Akonadi::Attribute *clone() const;
+    QByteArray type() const;
+
+private:
+    QByteArray mLabel;
+};
+
+#endif // GMAILLABELATTRIBUTE_H
diff --git a/resources/gmail/gmaillinkitemstask.cpp b/resources/gmail/gmaillinkitemstask.cpp
new file mode 100644 (file)
index 0000000..45814a1
--- /dev/null
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#include "gmaillinkitemstask.h"
+#include "gmailresource.h"
+#include "gmailretrieveitemstask.h"
+#include "gmailsettings.h"
+
+#include <AkonadiCore/collectionpathresolver.h>
+#include <AkonadiAgentBase/AgentBase>
+#include <AkonadiCore/ItemFetchJob>
+#include <AkonadiCore/ItemFetchScope>
+#include <AkonadiCore/CollectionFetchJob>
+#include <AkonadiCore/LinkJob>
+#include <AkonadiCore/UnlinkJob>
+
+#include <KLocalizedString>
+
+#define LABEL_PROPERTY "LabelProperty"
+#define COLLECTION_NAME_PROPERTY "CollectionNameProperty"
+#define COLLECTION_PROPERTY "CollectionProperty"
+
+GmailLinkItemsTask::GmailLinkItemsTask(GmailRetrieveItemsTask *retrieveTask, GmailResource *parent)
+    : QObject(parent)
+    , mResource(parent)
+{
+    connect(retrieveTask, SIGNAL(linkItem(QString,QVector<QByteArray>)),
+            this, SLOT(linkItem(QString,QVector<QByteArray>)));
+    connect(retrieveTask, SIGNAL(destroyed(QObject*)),
+            this, SLOT(onRetrievalDone()));
+
+}
+
+GmailLinkItemsTask::~GmailLinkItemsTask()
+{
+
+}
+
+void GmailLinkItemsTask::emitDone()
+{
+    Q_EMIT done();
+    Q_EMIT status(Akonadi::AgentBase::Idle);
+    deleteLater();
+}
+
+void GmailLinkItemsTask::linkItem(const QString &remoteId, const QVector<QByteArray> &labels)
+{
+    mLinkMap.insert(remoteId, labels);
+    Q_FOREACH (const QByteArray &label, labels) {
+        if (!mLabels.contains(label)) {
+            mLabels << label;
+        }
+    }
+}
+
+void GmailLinkItemsTask::onRetrievalDone()
+{
+    Q_EMIT status(Akonadi::AgentBase::Running, i18n("Linking emails to labels"));
+    if (!mLabels.isEmpty()) {
+        resolveNextLabel();
+    } else {
+        emitDone();
+    }
+}
+
+// Step 1: Resolve all labels to collections
+void GmailLinkItemsTask::resolveNextLabel()
+{
+    const QByteArray label = mLabels.takeFirst();
+
+    QString realCollectionName;
+    if (label == "\\Inbox") {
+        realCollectionName = QLatin1String("/INBOX");
+    } else if (label == "\\Drafts" || label == "\\Draft") {
+        realCollectionName = QLatin1String("/Drafts");
+    } else if (label == "\\Important") {
+        realCollectionName = QLatin1String("/Important");
+    } else if (label == "\\Sent") {
+        realCollectionName = QLatin1String("/Sent Mail");
+    } else if (label == "\\Junk" || label == "\\Spam") {
+        realCollectionName = QLatin1String("/Spam");
+    } else if (label == "\\Flagged" || label == "\\Starred") {
+        realCollectionName = QLatin1String("/Starred");
+    } else if (label == "\\Trash") {
+        realCollectionName = QLatin1String("/Trash");
+    } else if (label[0] == '/') {
+        realCollectionName = QString::fromLatin1(label);
+    } else {
+        realCollectionName = QLatin1Char('/') + QString::fromLatin1(label);
+    }
+
+    Akonadi::Collection rootCollection;
+    rootCollection.setRemoteId(mResource->settings()->rootRemoteId());
+    Akonadi::CollectionPathResolver *resolver
+        = new Akonadi::CollectionPathResolver(realCollectionName, rootCollection, this);
+    resolver->setProperty(COLLECTION_NAME_PROPERTY, realCollectionName);
+    resolver->setProperty(LABEL_PROPERTY, label);
+    connect(resolver, SIGNAL(finished(KJob*)),
+            this, SLOT(onLabelResolved(KJob*)));
+}
+
+// Step 2: Continue resolving until all is resolved, then go to step 3
+void GmailLinkItemsTask::onLabelResolved(KJob *job)
+{
+    Akonadi::CollectionPathResolver *resolver
+        = qobject_cast<Akonadi::CollectionPathResolver *>(job);
+    const QString collectionName = resolver->property(COLLECTION_NAME_PROPERTY).toString();
+    const QByteArray label = resolver->property(LABEL_PROPERTY).toByteArray();
+    if (resolver->error() && resolver->collection() < 0) {
+        qWarning() << "Failed to resolve collection ID for path" << collectionName << ":" << resolver->errorString();
+        return;
+    }
+    const Akonadi::Collection collection(resolver->collection());
+    mLabelCollectionMap.insert(label, collection);
+
+    if (!mLabels.isEmpty()) {
+        resolveNextLabel();
+    } else {
+        retrieveVirtualReferences();
+    }
+}
+
+// Step 3: Retrieve virtual references ot all our items
+void GmailLinkItemsTask::retrieveVirtualReferences()
+{
+    Akonadi::Collection allMailCollection;
+    allMailCollection.setRemoteId(QLatin1String("/[Gmail]/All Mail"));
+
+    Akonadi::Item::List items;
+    Q_FOREACH (const QString &remoteId, mLinkMap.uniqueKeys()) {
+        Akonadi::Item item;
+        item.setRemoteId(remoteId);
+        items << item;
+    }
+    Akonadi::ItemFetchJob *fetchJob = new Akonadi::ItemFetchJob(items, this);
+    fetchJob->setCollection(allMailCollection);
+    fetchJob->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::None);
+    fetchJob->fetchScope().setCacheOnly(true);
+    fetchJob->fetchScope().setFetchModificationTime(false);
+    fetchJob->fetchScope().setFetchRemoteIdentification(true);
+    fetchJob->fetchScope().setFetchVirtualReferences(true);
+    connect(fetchJob, SIGNAL(finished(KJob*)),
+            this, SLOT(onVirtualReferencesRetrieved(KJob*)));
+}
+
+// Step 4: Compare existing virtual references and our current labels, link and
+// unlink as necessary
+void GmailLinkItemsTask::onVirtualReferencesRetrieved(KJob *job)
+{
+    if (job->error()) {
+        // TODO: Error handling
+        emitDone();
+        return;
+    }
+
+    Akonadi::ItemFetchJob *fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job);
+
+    QMap<Akonadi::Collection, Akonadi::Item::List> toLink;
+    QMap<Akonadi::Collection, Akonadi::Item::List> toUnlink;
+    Q_FOREACH (const Akonadi::Item &item, fetchJob->items()) {
+        Akonadi::Collection::List existingReferences = item.virtualReferences();
+        const QVector<QByteArray> newLabels = mLinkMap[item.remoteId()];
+        Akonadi::Collection::List newReferences;
+        Q_FOREACH (const QByteArray &label, newLabels) {
+            const Akonadi::Collection newRef = mLabelCollectionMap[label];
+            if (!existingReferences.contains(newRef)) {
+                Akonadi::Item::List &list = toLink[newRef];
+                list << item;
+            } else {
+                existingReferences.removeOne(newRef);
+            }
+        }
+        if (!existingReferences.isEmpty()) {
+            Q_FOREACH (const Akonadi::Collection &ref, existingReferences) {
+                Akonadi::Item::List &list = toUnlink[ref];
+                list << item;
+            }
+        }
+    }
+
+    QMap<Akonadi::Collection, Akonadi::Item::List>::ConstIterator iter;
+    for (iter = toLink.constBegin(); iter != toLink.constEnd(); ++iter) {
+        new Akonadi::LinkJob(iter.key(), iter.value());
+    }
+    for (iter = toUnlink.constBegin(); iter != toUnlink.constEnd(); ++iter) {
+        new Akonadi::UnlinkJob(iter.key(), iter.value());
+    }
+
+    emitDone();
+}
+
diff --git a/resources/gmail/gmaillinkitemstask.h b/resources/gmail/gmaillinkitemstask.h
new file mode 100644 (file)
index 0000000..c2a0b7f
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifndef GMAILLINKITEMSTASK_H
+#define GMAILLINKITEMSTASK_H
+
+#include <QObject>
+#include <QMultiHash>
+
+#include <AkonadiCore/Collection>
+
+class GmailResource;
+class GmailRetrieveItemsTask;
+class KJob;
+
+class GmailLinkItemsTask : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit GmailLinkItemsTask(GmailRetrieveItemsTask *retrieveTask, GmailResource *parent0);
+    virtual ~GmailLinkItemsTask();
+
+Q_SIGNALS:
+    void done();
+    void status(int status, const QString &message = QString());
+
+private Q_SLOTS:
+    void linkItem(const QString &remoteId, const QVector<QByteArray> &labels);
+
+    void onRetrievalDone();
+    void resolveNextLabel();
+    void onLabelResolved(KJob *job);
+    void retrieveVirtualReferences();
+    void onVirtualReferencesRetrieved(KJob *job);
+
+private:
+    void emitDone();
+
+    QHash<QString, QVector<QByteArray> > mLinkMap;
+    QList<QByteArray> mLabels;
+    QMap<QByteArray, Akonadi::Collection> mLabelCollectionMap;
+
+    GmailResource *mResource;
+};
+
+#endif // GMAILLINKITEMSTASK_H
diff --git a/resources/gmail/gmailmessagehelper.cpp b/resources/gmail/gmailmessagehelper.cpp
new file mode 100644 (file)
index 0000000..05ad9b1
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#include "gmailmessagehelper.h"
+#include "gmailretrieveitemstask.h"
+
+GmailMessageHelper::GmailMessageHelper(const Akonadi::Collection &collection, ResourceTask *currentTask)
+    : MessageHelper()
+    , mCollection(collection)
+    , mTask(currentTask)
+{
+
+}
+
+Akonadi::Item GmailMessageHelper::createItemFromMessage(KMime::Message::Ptr message,
+        const qint64 uid,
+        const qint64 size,
+        const QList<KIMAP::MessageAttribute> &attrs,
+        const QList<QByteArray> &flags,
+        const KIMAP::FetchJob::FetchScope &scope,
+        bool &ok) const
+{
+    Akonadi::Item item = MessageHelper::createItemFromMessage(message, uid, size, attrs, flags, scope, ok);
+    if (!ok) {
+        qWarning() << "Failed to read imap message";
+        return item;
+    }
+
+    Q_FOREACH (const KIMAP::MessageAttribute &attr, attrs) {
+        if (attr.first == "X-GM-LABELS") {
+            if (mTask) {
+                QVector<QByteArray> labels;
+
+                QByteArray labelStr = attr.second.toByteArray();
+                int lastPos = 0;
+                bool isQuoted = false;
+                int i = 0;
+                while (i < labelStr.size()) {
+                    if (labelStr[i] == '(') {
+                        lastPos = i;
+                        i++;
+                        continue;
+                    }
+
+                    if (labelStr[i] == '\"') {
+                        lastPos = i;
+                        isQuoted = true;
+                        i++;
+                    }
+
+                    if (isQuoted) {
+                        while (labelStr[i] != '\"') {
+                            if (i == labelStr.size()) {
+                                // huh? Broken string?
+                                break;
+                            }
+                            i++;
+                        }
+                        QByteArray mid = labelStr.mid(lastPos + 1, i - lastPos - 1).trimmed();
+                        if (!mid.isEmpty() && mid != "\\\\All") {
+                            labels.append(mid.replace("\\\\", "\\"));
+                        }
+                        isQuoted = false;
+                        lastPos = i;
+                    } else {
+                        if (labelStr[i] == ' ' || labelStr[i] == ')') {
+                            QByteArray mid = labelStr.mid(lastPos + 1, i - lastPos - 1).trimmed();
+                            if (!mid.isEmpty() && mid != "\\\\All") {
+                                labels.append(mid.replace("\\\\", "\\"));
+                            }
+                            lastPos = i;
+                        }
+                    }
+                    ++i;
+                }
+                if (!labels.isEmpty()) {
+                    GmailRetrieveItemsTask *task = qobject_cast<GmailRetrieveItemsTask *>(mTask);
+                    Q_ASSERT(task);
+                    task->linkItem(item.remoteId(), labels);
+                }
+            }
+        } else if (attr.first == "X-GM-THRID") {
+            // TODO: Store thread information
+        } else if (attr.first == "X-GM-MSGID") {
+            item.setGid(attr.second.toString());
+        }
+    }
+
+    return item;
+}
diff --git a/resources/gmail/gmailmessagehelper.h b/resources/gmail/gmailmessagehelper.h
new file mode 100644 (file)
index 0000000..4be1193
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifndef GMAILMESSAGEHELPER_H
+#define GMAILMESSAGEHELPER_H
+
+#include <imap/messagehelper.h>
+
+#include <AkonadiCore/Collection>
+
+class ResourceTask;
+
+class GmailMessageHelper : public MessageHelper
+{
+public:
+    GmailMessageHelper(const Akonadi::Collection &collection, ResourceTask *currentTask);
+
+    virtual Akonadi::Item createItemFromMessage(KMime::Message::Ptr message,
+            const qint64 uid,
+            const qint64 size,
+            const QList<KIMAP::MessageAttribute> &attrs,
+            const QList<QByteArray> &flags,
+            const KIMAP::FetchJob::FetchScope &scope,
+            bool &ok) const;
+
+private:
+    Akonadi::Collection mCollection;
+    ResourceTask *mTask;
+};
+
+#endif // GMAILMESSAGEHELPER_H
diff --git a/resources/gmail/gmailpasswordrequester.cpp b/resources/gmail/gmailpasswordrequester.cpp
new file mode 100644 (file)
index 0000000..2ee341b
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#include "gmailpasswordrequester.h"
+#include "gmailsettings.h"
+#include "gmailresource.h"
+
+#include <kgapi/account.h>
+#include <kgapi/authjob.h>
+
+#include <QJsonDocument>
+
+#include <QDebug>
+
+/**
+ * See https://developers.google.com/gmail/xoauth2_protocol for protocol documentation
+ */
+
+GmailPasswordRequester::GmailPasswordRequester(GmailResource *resource, QObject *parent)
+    : PasswordRequesterInterface(parent)
+    , mResource(resource)
+{
+}
+
+GmailPasswordRequester::~GmailPasswordRequester()
+{
+}
+
+void GmailPasswordRequester::requestPassword(PasswordRequesterInterface::RequestType request, const QString &serverError)
+{
+    if (request == PasswordRequesterInterface::WrongPasswordRequest) {
+        connect(mResource->settings(), SIGNAL(accountRequestCompleted(KGAPI2::AccountPtr,bool)),
+                this, SLOT(onAuthFinished(KGAPI2::AccountPtr,bool)),
+                Qt::UniqueConnection);
+        static_cast<GmailSettings *>(mResource->settings())->requestAccount(true);
+    } else {
+        QMetaObject::invokeMethod(this, "done", Qt::QueuedConnection,
+                                  Q_ARG(int, PasswordRetrieved),
+                                  Q_ARG(QString, mResource->settings()->password()));
+    }
+}
+
+bool GmailPasswordRequester::isTokenExpired(const QString &serverError)
+{
+    QString base64Error = serverError.mid(7, serverError.length() - 9);
+    const QByteArray decoded = QByteArray::fromBase64(base64Error.toLatin1());
+    QJsonDocument document = QJsonDocument::fromJson(decoded);
+    if (document.isNull()) {
+        return false;
+    }
+    const QVariant json = document.toVariant();
+    if (!json.isValid()) {
+        return false;
+    }
+    const QVariantMap map = json.toMap();
+    if (map[QLatin1String("status")].toString().toInt() == KGAPI2::Unauthorized) {
+        return true;
+    }
+
+    qDebug() << "Gmail Auth error:" << json;
+    return false;
+}
+
+void GmailPasswordRequester::onAuthFinished(const KGAPI2::AccountPtr &account, bool userRejected)
+{
+    if (userRejected) {
+        done(UserRejected);
+        return;
+    }
+
+    if (!account) {
+        // Really??
+        done(ReconnectNeeded);
+        return;
+    }
+
+    // Access Token is not really a password, but meh...
+    done(PasswordRetrieved, account->accessToken());
+}
diff --git a/resources/gmail/gmailpasswordrequester.h b/resources/gmail/gmailpasswordrequester.h
new file mode 100644 (file)
index 0000000..ea706e0
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifndef GMAILPASSWORDREQUESTER_H
+#define GMAILPASSWORDREQUESTER_H
+
+#include <imap/passwordrequesterinterface.h>
+
+#include <KGAPI/Types>
+
+class GmailResource;
+class GmailPasswordRequester : public PasswordRequesterInterface
+{
+    Q_OBJECT
+
+public:
+    GmailPasswordRequester(GmailResource *resource, QObject *parent);
+    virtual ~GmailPasswordRequester();
+
+    virtual void requestPassword(RequestType request = StandardRequest, const QString &serverError = QString());
+
+private:
+    bool isTokenExpired(const QString &serverError);
+    QString encodePassword(const QString &accountName, const QString &accessToken) const;
+
+private Q_SLOTS:
+    void onAuthFinished(const KGAPI2::AccountPtr &account, bool userRejected);
+
+private:
+    GmailResource *mResource;
+};
+
+#endif // GMAILPASSWORDREQUESTER_H
diff --git a/resources/gmail/gmailresource.cpp b/resources/gmail/gmailresource.cpp
new file mode 100644 (file)
index 0000000..013d7ac
--- /dev/null
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+#include "gmailresource.h"
+#include "gmailretrievecollectionstask.h"
+#include "gmailretrieveitemstask.h"
+#include "gmailresourcestate.h"
+#include "gmailpasswordrequester.h"
+#include "gmailconfigdialog.h"
+#include "gmailsettings.h"
+#include "gmaillinkitemstask.h"
+#include "gmaillabelattribute.h"
+#include "gmailchangeitemslabelstask.h"
+
+#include <KLocalizedString>
+#include <KWindowSystem>
+
+#include <AkonadiCore/LinkJob>
+#include <AkonadiCore/CollectionFetchJob>
+#include <AkonadiCore/collectionpathresolver.h>
+#include <AkonadiCore/ItemFetchJob>
+#include <AkonadiCore/ItemFetchScope>
+#include <AkonadiCore/AttributeFactory>
+#include <AkonadiCore/specialcollections.h>
+
+#include <imap/sessionpool.h>
+#include <imap/sessionuiproxy.h>
+
+#include <QIcon>
+
+GmailResource::GmailResource(const QString &id)
+    : ImapResourceBase(id)
+    , m_settings(0)
+{
+    setSeparatorCharacter(QLatin1Char('/'));
+
+    m_pool->setPasswordRequester(new GmailPasswordRequester(this, m_pool));
+    m_pool->setSessionUiProxy(SessionUiProxy::Ptr(new SessionUiProxy));
+
+    Akonadi::AttributeFactory::registerAttribute<GmailLabelAttribute>();
+}
+
+GmailResource::~GmailResource()
+{
+}
+
+Settings *GmailResource::settings() const
+{
+    if (m_settings == 0) {
+        m_settings = new GmailSettings;
+    }
+
+    return m_settings;
+}
+
+QString GmailResource::defaultName() const
+{
+    return i18n("Gmail Resource");
+}
+
+QDialog *GmailResource::createConfigureDialog(WId windowId)
+{
+    GmailConfigDialog *dlg = new GmailConfigDialog(this, windowId);
+    KWindowSystem::setMainWindow(dlg, windowId);
+    dlg->setWindowIcon(QIcon::fromTheme(QLatin1String("network-server")));
+    connect(dlg, &QDialog::finished, this, &GmailResource::onConfigurationDone);;
+    return dlg;
+}
+
+void GmailResource::onConfigurationDone(int result)
+{
+    GmailConfigDialog *dlg = qobject_cast<GmailConfigDialog *>(sender());
+    if (result) {
+        if (dlg->shouldClearCache()) {
+            clearCache();
+        }
+        settings()->save();
+    }
+    dlg->deleteLater();
+}
+
+Akonadi::Collection GmailResource::allMailCollection() const
+{
+    Akonadi::Collection c;
+    c.setRemoteId(QLatin1String("/[Gmail]/All Mail"));
+    return c;
+}
+
+Akonadi::Collection GmailResource::rootCollection() const
+{
+    Akonadi::Collection c;
+    c.setRemoteId(settings()->rootRemoteId());
+    return c;
+}
+
+ResourceStateInterface::Ptr GmailResource::createResourceState(const TaskArguments &args)
+{
+    return ResourceStateInterface::Ptr(new GmailResourceState(this, args));
+}
+
+void GmailResource::retrieveCollections()
+{
+    emit status(AgentBase::Running, i18nc("@info:status", "Retrieving folders"));
+
+    ResourceTask *task = new GmailRetrieveCollectionsTask(createResourceState(TaskArguments()), this);
+    if (settings()->trashCollection() == -1) {
+        connect(task, SIGNAL(destroyed(QObject*)),
+                this, SLOT(updateTrashFolder()));
+    }
+    task->start(m_pool);
+    queueTask(task);
+}
+
+void GmailResource::updateTrashFolder()
+{
+    Akonadi::CollectionFetchJob *fetch
+        = new Akonadi::CollectionFetchJob(rootCollection(), Akonadi::CollectionFetchJob::FirstLevel, this);
+    connect(fetch, &KJob::finished,
+            this, &GmailResource::onUpdateTrashFolderCollectionsRetrieved);
+}
+
+void GmailResource::onUpdateTrashFolderCollectionsRetrieved(KJob *job)
+{
+    Akonadi::CollectionFetchJob *fetch = qobject_cast<Akonadi::CollectionFetchJob *>(job);
+    if (job->error()) {
+        kError() << fetch->errorString();
+        return;
+    }
+
+    Akonadi::Collection::List cols = fetch->collections();
+    Q_FOREACH (const Akonadi::Collection &col, cols) {
+        GmailLabelAttribute *attr = col.attribute<GmailLabelAttribute>();
+        if (!attr) {
+            continue;
+        }
+
+        if (attr->isTrash()) {
+            settings()->setTrashCollection(col.id());
+            Akonadi::SpecialCollections::setSpecialCollectionType("trash", col);
+            return;
+        }
+    }
+
+    kWarning() << "Failed to detect the Trash folder...!?";
+}
+
+void GmailResource::retrieveItems(const Akonadi::Collection &col)
+{
+    qDebug() << col.id() << col.remoteId() << col.name();
+    // We can't sync the virtual collections - instead we get ID of "All Mail" and
+    // we schedule it's sync
+    //
+    // TODO: Don't resync the All Mail collections X times in a row for each virtual
+    // collection, instead uset a timer or something
+    if (col.isVirtual()) {
+        Akonadi::CollectionFetchJob *fetch
+            = new Akonadi::CollectionFetchJob(allMailCollection(), Akonadi::CollectionFetchJob::Base, this);
+        connect(fetch, SIGNAL(finished(KJob*)),
+                this, SLOT(onRetrieveItemsCollectionRetrieved(KJob*)));
+        return;
+    }
+
+    setItemStreamingEnabled(true);
+
+    GmailRetrieveItemsTask *task = new GmailRetrieveItemsTask(createResourceState(TaskArguments(col)), this);
+    connect(task, SIGNAL(status(int,QString)), SIGNAL(status(int,QString)));
+    connect(this, SIGNAL(retrieveNextItemSyncBatch(int)), task, SLOT(onReadyForNextBatch(int)));
+
+    GmailLinkItemsTask *linkTask = new GmailLinkItemsTask(task, this);
+    connect(linkTask, SIGNAL(status(int,QString)), SIGNAL(status(int,QString)));
+
+    startTask(task);
+    scheduleCustomTask(this, "triggerCollectionExtraInfoJobs", QVariant::fromValue(col), ResourceBase::Append);
+}
+
+void GmailResource::onRetrieveItemsCollectionRetrieved(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(job->errorString());
+        return;
+    }
+
+    Akonadi::CollectionFetchJob *fetch = qobject_cast<Akonadi::CollectionFetchJob *>(job);
+    if (fetch->collections().count() != 1) {
+        qWarning() << "Got" << fetch->collections().count() << "collections, expected only one!";
+        cancelTask();
+        return;
+    }
+
+    synchronizeCollection(fetch->collections().at(0).id());
+
+    itemsRetrievalDone();
+}
+
+void GmailResource::itemsLinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection)
+{
+    if (!collection.hasAttribute<GmailLabelAttribute>()) {
+        qWarning() << "Collection is missing GmailLabelAttribute! IMPOSSIBRU!";
+        cancelTask();
+        return;
+    }
+
+    const QByteArray label = collection.attribute<GmailLabelAttribute>()->label();
+    TaskArguments args(items, QSet<QByteArray>() << label, QSet<QByteArray>());
+    GmailChangeItemsLabelsTask *task = new GmailChangeItemsLabelsTask(createResourceState(args), this);
+    startTask(task);
+}
+
+void GmailResource::itemsUnlinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection)
+{
+    if (!collection.hasAttribute<GmailLabelAttribute>()) {
+        qWarning() << "Collection is missing GmailLabelAttribute! IMPOSSIBRU!";
+        cancelTask();
+        return;
+    }
+
+    const QByteArray label = collection.attribute<GmailLabelAttribute>()->label();
+    TaskArguments args(items, QSet<QByteArray>(), QSet<QByteArray>() << label);
+    GmailChangeItemsLabelsTask *task = new GmailChangeItemsLabelsTask(createResourceState(args), this);
+    startTask(task);
+}
+
+AKONADI_RESOURCE_MAIN(GmailResource)
diff --git a/resources/gmail/gmailresource.desktop b/resources/gmail/gmailresource.desktop
new file mode 100644 (file)
index 0000000..dbad675
--- /dev/null
@@ -0,0 +1,80 @@
+[Desktop Entry]
+Name=Gmail
+Name[ast]=Gmail
+Name[bg]=Gmail
+Name[ca]=Gmail
+Name[ca@valencia]=Gmail
+Name[da]=Gmail
+Name[de]=Gmail
+Name[el]=Gmail
+Name[en_GB]=Gmail
+Name[es]=Gmail
+Name[et]=Gmail
+Name[fi]=Gmail
+Name[fr]=Gmail
+Name[gl]=Gmail
+Name[hu]=Gmail
+Name[ia]=Gmail
+Name[it]=Gmail
+Name[ko]=GMail
+Name[nb]=Gmail
+Name[nds]=GMail
+Name[nl]=Gmail
+Name[pl]=Gmail
+Name[pt]=Gmail
+Name[pt_BR]=Gmail
+Name[ru]=Gmail
+Name[sk]=Gmail
+Name[sl]=Gmail
+Name[sr]=Г‑мејл
+Name[sr@ijekavian]=Г‑мејл
+Name[sr@ijekavianlatin]=GMail
+Name[sr@latin]=GMail
+Name[sv]=Gmail
+Name[tr]=Gmail
+Name[uk]=Gmail
+Name[x-test]=xxGmailxx
+Name[zh_CN]=Gmail
+Name[zh_TW]=Gmail
+Comment=Connects to your Gmail account
+Comment[bg]=Свързване към вашата сметка в Google
+Comment[ca]=Connecta amb el vostre compte de Gmail
+Comment[ca@valencia]=Connecta amb el vostre compte de Gmail
+Comment[da]=Forbinder til din Gmail-konto
+Comment[de]=Verbindet mit Ihrem Gmail-Konto
+Comment[el]=Σύνδεση με τον λογαριασμό σας στο Gmail
+Comment[en_GB]=Connects to your Gmail account
+Comment[es]=Conecta a su cuenta Gmail
+Comment[et]=Ühendumine Gmaili kontoga
+Comment[fi]=Yhdistää Gmail-tiliin
+Comment[fr]=Se connecte à votre compte Gmail
+Comment[gl]=Conecta á súa conta de Gmail
+Comment[hu]=Csatlakozik a Gmail fiókjához
+Comment[it]=Connette al tuo account Gmail
+Comment[ko]=내 GMail 계정에 연결
+Comment[nb]=Kobler til Gmail-kontoen din
+Comment[nds]=Koppelt sik Dien GMail-Konto to
+Comment[nl]=Verbindt met uw Gmail account
+Comment[pl]=Łączy z kontem Gmail
+Comment[pt]=Liga-se à sua conta de Gmail
+Comment[pt_BR]=Conecta-o a sua conta do Gmail
+Comment[ru]=Подключение к учётной записи Gmail
+Comment[sk]=Pripojí k vášmu Gmail účtu
+Comment[sl]=Poveže se z vašim računom za Gmail
+Comment[sr]=Повезивање са налогом на Г‑мејлу
+Comment[sr@ijekavian]=Повезивање са налогом на Г‑мејлу
+Comment[sr@ijekavianlatin]=Povezivanje sa nalogom na GMailu
+Comment[sr@latin]=Povezivanje sa nalogom na GMailu
+Comment[sv]=Ansluter till ditt Gmail-konto
+Comment[tr]=Gmail hesabına bağlanır
+Comment[uk]=Встановлює з’єднання з вашим обліковим записом Gmail
+Comment[x-test]=xxConnects to your Gmail accountxx
+Comment[zh_CN]=连接到您的 Gmail 账户
+Comment[zh_TW]=連線到您的 Gmail 帳號
+Type=AkonadiResource
+Exec=akonadi_gmail_resource
+Icon=im-google
+
+X-Akonadi-MimeTypes=message/rfc822
+X-Akonadi-Capabilities=Resource
+X-Akonadi-Identifier=akonadi_gmail_resource
diff --git a/resources/gmail/gmailresource.h b/resources/gmail/gmailresource.h
new file mode 100644 (file)
index 0000000..5e94ff6
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+#ifndef GMAILRESOURCE_H
+#define GMAILRESOURCE_H
+
+#include <imap/imapresourcebase.h>
+
+#include <AkonadiAgentBase/AgentBase>
+
+class GmailSettings;
+
+class GmailResource : public ImapResourceBase
+{
+    Q_OBJECT
+
+    using Akonadi::AgentBase::ObserverV3;
+
+public:
+    explicit GmailResource(const QString &id);
+    ~GmailResource();
+
+    QDialog *createConfigureDialog(WId windowId);
+    Akonadi::Collection allMailCollection() const;
+    Akonadi::Collection rootCollection() const;
+
+    QString defaultName() const;
+
+    ResourceStateInterface::Ptr createResourceState(const TaskArguments &args);
+
+    void retrieveCollections();
+    void retrieveItems(const Akonadi::Collection &col);
+
+    void itemsLinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection);
+    void itemsUnlinked(const Akonadi::Item::List &items, const Akonadi::Collection &collection);
+
+    Settings *settings() const;
+
+private Q_SLOTS:
+    void updateTrashFolder();
+    void onUpdateTrashFolderCollectionsRetrieved(KJob *job);
+
+    void onConfigurationDone(int result);
+    void onRetrieveItemsCollectionRetrieved(KJob *job);
+
+private:
+    mutable GmailSettings *m_settings;
+};
+
+#endif // GMAILRESOURCE_H
diff --git a/resources/gmail/gmailresource.kcfg b/resources/gmail/gmailresource.kcfg
new file mode 100644 (file)
index 0000000..b399efb
--- /dev/null
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:kcfg="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
+      http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
+  <kcfgfile/>
+  <group name="network">
+    <entry name="ImapServer" type="String">
+      <label>IMAP server</label>
+      <default>imap.gmail.com</default>
+    </entry>
+    <entry name="ImapPort"  type="Int">
+      <label>Defines the port the IMAP service is running on</label>
+      <default>993</default>
+    </entry>
+    <entry name="UserName" type="String">
+      <label>Username</label>
+    </entry>
+    <entry name="Safety" type="String">
+      <label>Defines the encryption type to use</label>
+      <default>SSL</default>
+    </entry>
+    <entry name="OverrideEncryption" type="String">
+      <label>Override configured encryption mode</label>
+      <default></default>
+    </entry>
+    <entry name="Authentication" type="Int">
+      <label>Defines the authentication type to use</label>
+      <default>1</default>
+    </entry>
+    <entry name="SubscriptionEnabled" type="Bool">
+      <label>Defines if the server side subscription is enabled</label>
+      <default>false</default>
+    </entry>
+    <entry name="SessionTimeout" type="Int">
+      <default>30</default>
+    </entry>
+  </group>
+  <group name="cache">
+    <entry name="DisconnectedModeEnabled" type="Bool">
+      <label>Defines if all the IMAP data should be cached locally all the time</label>
+      <default>false</default>
+    </entry>
+    <entry name="IntervalCheckEnabled" type="Bool">
+       <label>Defines if interval checking is enabled.</label>
+       <default>true</default>
+     </entry>
+    <entry name="IntervalCheckTime" type="Int">
+      <label>Check interval in minutes</label>
+      <default>5</default>
+    </entry>
+    <entry name="RetrieveMetadataOnFolderListing" type="Bool">
+      <label>Defines if the annotations, ACLs and quota information of mailboxes should
+             also be retrieved when the mailboxes get listed.</label>
+      <default>true</default>
+    </entry>
+    <entry name="AutomaticExpungeEnabled" type="Bool">
+      <label>Defines if the expunge command is issued automatically, otherwise it should be
+             triggered manually through the D-Bus interface.</label>
+      <default>true</default>
+    </entry>
+    <entry name="TrashCollection" type="LongLong">
+       <label>Define which folder is used for trash</label>
+       <default>-1</default>
+    </entry>
+    <entry name="TrashCollectionMigrated" type="Bool">
+       <label>Define if the trash collection received the special attribute</label>
+       <default>false</default>
+    </entry>
+    <entry name="UseDefaultIdentity" type="Bool">
+       <label>Define if account uses the default identity</label>
+       <default>true</default>
+    </entry>
+    <entry name="AccountIdentity" type="Int">
+       <label>Identity account</label>
+    </entry>
+    <entry name="KnownMailBoxes" type="StringList">
+      <label>List of mailbox names reported by the server the last time</label>
+    </entry>
+  </group>
+  <group name="idle">
+    <entry name="IdleRidPath" type="StringList">
+      <label>RID path to the mailbox to watch for changes</label>
+    </entry>
+  </group>
+  <group name="siever">
+    <entry name="SieveSupport" type="Bool">
+      <label>Define if server supports sieve</label>
+      <default>false</default>
+    </entry>
+    <entry name="SieveReuseConfig" type="Bool">
+      <label>Define if we reuse host and login configuration</label>
+      <default>true</default>
+    </entry>
+    <entry name="SievePort" type="Int">
+      <label>Define sieve port</label>
+      <default>4190</default>
+    </entry>
+    <entry name="SieveAlternateUrl" type="String">
+      <label>Define alternate URL</label>
+    </entry>
+    <entry name="SieveVacationFilename" type="String">
+      <label>Define default sieve vacation filename</label>
+      <default>kmail-vacation.siv</default>
+    </entry>
+    <entry name="SieveCustomUsername" type="String">
+      <label>Define username used from custom server sieve url</label>
+      <default></default>
+    </entry>
+    <entry name="SieveCustomAuthentification" type="String">
+      <label>Defines the type of identification used by custom sieve server</label>
+      <default>ImapUserPassword</default>
+    </entry>
+  </group>
+</kcfg>
diff --git a/resources/gmail/gmailresourcestate.cpp b/resources/gmail/gmailresourcestate.cpp
new file mode 100644 (file)
index 0000000..14e07ed
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#include "gmailresourcestate.h"
+#include "gmailmessagehelper.h"
+#include "gmailretrieveitemstask.h"
+#include "gmailresource.h"
+
+#include <imap/resourcetask.h>
+
+GmailResourceState::GmailResourceState(GmailResource *resource, const TaskArguments &arguments)
+    : ResourceState(resource, arguments)
+    , mResource(resource)
+    , mTask(0)
+{
+}
+
+void GmailResourceState::setCurrentTask(ResourceTask *task)
+{
+    mTask = task;
+}
+
+ResourceTask *GmailResourceState::currentTask() const
+{
+    return mTask;
+}
+
+MessageHelper::Ptr GmailResourceState::messageHelper() const
+{
+    return MessageHelper::Ptr(new GmailMessageHelper(collection(),
+                              qobject_cast<GmailRetrieveItemsTask *>(mTask)));
+}
diff --git a/resources/gmail/gmailresourcestate.h b/resources/gmail/gmailresourcestate.h
new file mode 100644 (file)
index 0000000..a90efc4
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifndef GMAILRESOURCESTATE_H
+#define GMAILRESOURCESTATE_H
+
+#include <imap/resourcestate.h>
+
+class GmailResource;
+class ResourceTask;
+
+class GmailResourceState : public ResourceState
+{
+public:
+    explicit GmailResourceState(GmailResource *resource, const TaskArguments &arguments);
+
+    void setCurrentTask(ResourceTask *task);
+    ResourceTask *currentTask() const;
+
+protected:
+    virtual MessageHelper::Ptr messageHelper() const;
+
+private:
+    GmailResource *mResource;
+    ResourceTask *mTask;
+};
+
+#endif // GMAILRESOURCESTATE_H
diff --git a/resources/gmail/gmailretrievecollectionstask.cpp b/resources/gmail/gmailretrievecollectionstask.cpp
new file mode 100644 (file)
index 0000000..ad96cc7
--- /dev/null
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+#include "gmailretrievecollectionstask.h"
+#include "gmaillabelattribute.h"
+
+#include <imap/noselectattribute.h>
+#include <imap/noinferiorsattribute.h>
+
+#include <AkonadiCore/CachePolicy>
+#include <AkonadiCore/EntityDisplayAttribute>
+#include <Akonadi/KMime/MessageParts>
+#include <AkonadiCore/EntityHiddenAttribute>
+
+#include <KMime/Message>
+
+#include <KLocalizedString>
+
+GmailRetrieveCollectionsTask::GmailRetrieveCollectionsTask(ResourceStateInterface::Ptr resource,
+        QObject *parent)
+    : RetrieveCollectionsTask(resource, parent)
+{
+}
+
+void GmailRetrieveCollectionsTask::onMailBoxesReceived(const QList<KIMAP::MailBoxDescriptor> &descriptors,
+        const QList<QList<QByteArray> > &flags)
+{
+    Akonadi::Collection &rootCollection = m_reportedCollections[QString()];
+    Akonadi::EntityDisplayAttribute *attr = rootCollection.attribute<Akonadi::EntityDisplayAttribute>(Akonadi::Entity::AddIfMissing);
+    attr->setIconName(QLatin1String("im-google"));
+    attr->setDisplayName(rootCollection.name());
+
+    QStringList contentTypes;
+    contentTypes << KMime::Message::mimeType();
+
+    for (int i = 0, dscCnt = descriptors.size(); i < dscCnt; ++i) {
+        const KIMAP::MailBoxDescriptor descriptor = descriptors[i];
+
+        // skip phantom mailboxes contained in LSUB but not LIST
+        if (isSubscriptionEnabled() && !m_fullReportedCollections.contains(descriptor.name)) {
+            qDebug() << "Got phantom mailbox: " << descriptor.name;
+            continue;
+        }
+
+        const QString boxName = descriptor.name.endsWith(separatorCharacter())
+                                ? descriptor.name.left(descriptor.name.size() - 1)
+                                : descriptor.name;
+
+        /* FIXME: The Chats folder contains logs of Hangouts chats, which is rather
+         * useless in Kmail, and also it breaks sync, because it is not a "special"
+         * folder. We should re-enable it at some point so that people can't
+         * complain, but until then the folder will not be synced.
+         */
+        if (boxName == QLatin1String("[Gmail]/Chats")) {
+            continue;
+        }
+
+        const QStringList pathParts = boxName.split(separatorCharacter());
+
+        QString parentPath;
+        QString currentPath;
+
+        for (int j = 0, partsCnt = pathParts.count(); j < partsCnt; ++j) {
+            const bool isDummy = j != pathParts.size() - 1;
+            const QString pathPart = pathParts.at(j);
+            currentPath += separatorCharacter() + pathPart;
+
+            if (m_reportedCollections.contains(currentPath)) {
+                if (m_dummyCollections.contains(currentPath) && !isDummy) {
+                    qDebug() << "Received the real collection for a dummy one : " << currentPath;
+                    //set the correct attributes for the collection, eg. noselect needs to be removed
+                    Akonadi::Collection c = m_reportedCollections.value(currentPath);
+                    c.setContentMimeTypes(contentTypes);
+                    c.setRights(Akonadi::Collection::AllRights);
+                    c.removeAttribute<NoSelectAttribute>();
+                    m_dummyCollections.remove(currentPath);
+                    m_reportedCollections.insert(currentPath, c);
+                }
+                parentPath = currentPath;
+                continue;
+            }
+
+            const QList<QByteArray> currentFlags = isDummy ? (QList<QByteArray>() << "\\noselect") : flags[i];
+
+            Akonadi::Collection c;
+            c.setName(pathPart);
+            c.setRemoteId(separatorCharacter() + pathPart);
+            const Akonadi::Collection parentCollection = m_reportedCollections.value(parentPath);
+            c.setParentCollection(parentCollection);
+            c.setContentMimeTypes(contentTypes);
+            c.setVirtual(true); // All collections are virtual
+            c.setRights(Akonadi::Collection::CanChangeCollection |
+                        Akonadi::Collection::CanDeleteCollection |
+                        Akonadi::Collection::CanCreateCollection |
+                        Akonadi::Collection::CanLinkItem |
+                        Akonadi::Collection::CanUnlinkItem |
+                        Akonadi::Collection::CanCreateItem |
+                        Akonadi::Collection::CanDeleteItem |
+                        Akonadi::Collection::CanChangeItem);
+
+            Akonadi::EntityDisplayAttribute *attr = c.attribute<Akonadi::EntityDisplayAttribute>(Akonadi::Entity::AddIfMissing);
+            if (currentFlags.contains("\\trash")) {
+                attr->setIconName(QLatin1String("user-trash"));
+            } else if (currentFlags.contains("\\sent")) {
+                attr->setIconName(QLatin1String("mail-folder-sent"));
+            } else if (currentFlags.contains("\\inbox")) {
+                attr->setIconName(QLatin1String("mail-folder-inbox"));
+            } else {
+                attr->setIconName(QLatin1String("folder"));
+            }
+            attr->setDisplayName(pathPart);
+
+            // If the folder is the Inbox, make some special settings.
+            if (currentPath.compare(separatorCharacter() + QLatin1String("INBOX"), Qt::CaseInsensitive) == 0) {
+                Akonadi::EntityDisplayAttribute *attr = c.attribute<Akonadi::EntityDisplayAttribute>(Akonadi::Collection::AddIfMissing);
+                attr->setDisplayName(i18n("Inbox"));
+                attr->setIconName(QLatin1String("mail-folder-inbox"));
+            }
+
+            // "All Mail", "Trash" and "Spam" are the only non-virtual collections
+            // that will store the actual emails.
+            if (currentFlags.contains("\\all") || currentFlags.contains("\\trash") || currentFlags.contains("\\junk")) {
+                c.setVirtual(false);
+                c.addAttribute(new NoSelectAttribute);
+                c.addAttribute(new NoInferiorsAttribute);
+                c.setParentCollection(m_reportedCollections.value(QString()));
+                c.setRemoteId(currentPath);
+                c.setLocalListPreference(Akonadi::Collection::ListSync, Akonadi::Collection::ListDefault);
+
+                // Hide "All mail" collection and mark it as IDLE
+                if (currentFlags.contains("\\all")) {
+                    c.setEnabled(false);
+                    c.setLocalListPreference(Akonadi::Collection::ListDisplay, Akonadi::Collection::ListDisabled);
+                    c.setLocalListPreference(Akonadi::Collection::ListIndex, Akonadi::Collection::ListDisabled);
+                    c.setLocalListPreference(Akonadi::Collection::ListSync, Akonadi::Collection::ListEnabled);
+
+                    // This mean that we will automatically pick up changes in
+                    // all labels - YAY!
+                    setIdleCollection(c);
+                }
+                c.setRights(Akonadi::Collection::CanCreateItem |
+                            Akonadi::Collection::CanChangeItem |
+                            Akonadi::Collection::CanDeleteItem);
+            }
+
+            // If this folder is a noselect folder, make some special settings.
+            if (currentFlags.contains("\\noselect")) {
+                qDebug() << "Dummy collection created: " << currentPath;
+                c.addAttribute(new NoSelectAttribute(true));
+                c.setContentMimeTypes(QStringList() << Akonadi::Collection::mimeType());
+                c.setRights(Akonadi::Collection::ReadOnly);
+            } else {
+                // remove the noselect attribute explicitly, in case we had set it before (eg. for non-subscribed non-leaf folders)
+                c.removeAttribute<NoSelectAttribute>();
+            }
+
+            // If this folder is a noinferiors folder, it is not allowed to create subfolders inside.
+            if (currentFlags.contains("\\noinferiors")) {
+                //qDebug() << "Noinferiors: " << currentPath;
+                c.addAttribute(new NoInferiorsAttribute(true));
+                c.setRights(c.rights() & ~Akonadi::Collection::CanCreateCollection);
+            }
+
+            qDebug() << currentPath << currentFlags;
+            // Special treating of Gmail system collections (and INBOX)
+            if (currentPath == QLatin1String("/INBOX") ||
+                    currentFlags.contains("\\drafts") ||
+                    currentFlags.contains("\\important") ||
+                    currentFlags.contains("\\sent") ||
+                    currentFlags.contains("\\flagged")) {
+                // Keep [Gmail] in remoteID, so that we can reference them correctly
+                // even though they have different parent in Akonadi
+                c.setRemoteId(currentPath);
+                // Move them from the [Gmail] subfolder
+                c.setParentCollection(m_reportedCollections.value(QString()));
+                // None of these can actually have subcollections, cannot be modified and
+                // cannot be removed.
+                c.setRights(c.rights() & ~Akonadi::Collection::CanDeleteCollection
+                            & ~Akonadi::Collection::CanChangeCollection
+                            & ~Akonadi::Collection::CanCreateCollection);
+            }
+
+            // I am the king of non-generic code!
+            if (currentFlags.contains("\\inbox") || pathPart == QLatin1String("INBOX")) {
+                c.addAttribute(new GmailLabelAttribute("\\Inbox"));
+            } else if (currentFlags.contains("\\drafts")) {
+                // This is not a typo, they actually use "\Draft" in X-GM-LABEL
+                c.addAttribute(new GmailLabelAttribute("\\Draft"));
+            } else if (currentFlags.contains("\\important")) {
+                c.addAttribute(new GmailLabelAttribute("\\Important"));
+            } else if (currentFlags.contains("\\sent")) {
+                c.addAttribute(new GmailLabelAttribute("\\Sent"));
+            } else if (currentFlags.contains("\\junk")) {
+                c.addAttribute(new GmailLabelAttribute("\\Junk"));
+            } else if (currentFlags.contains("\\flagged")) {
+                c.addAttribute(new GmailLabelAttribute("\\Flagged"));
+            } else if (currentFlags.contains("\\trash")) {
+                c.addAttribute(new GmailLabelAttribute("\\Trash"));
+            } else if (currentFlags.contains("\\all")) {
+                // Ignore
+            } else {
+                // For non-gmail flags, store the actual path without opening "/",
+                // which is actually Gmail label
+                c.addAttribute(new GmailLabelAttribute(currentPath.mid(1).toUtf8()));
+            }
+
+            // Add special mimetype to non-system virtual collections to allow
+            // creating subfolders
+            if (c.isVirtual() && c.rights() & Akonadi::Collection::CanCreateCollection) {
+                c.setContentMimeTypes(c.contentMimeTypes()
+                                      << Akonadi::Collection::virtualMimeType());
+            }
+
+            m_reportedCollections.insert(currentPath, c);
+
+            if (isDummy) {
+                m_dummyCollections.insert(currentPath, c);
+            }
+
+            parentPath = currentPath;
+        }
+    }
+
+    // Remove the [Gmail] folder. We inserted it only to get remoteIDs for it's subcollections right
+    // FIXME GMAIL: Don't hardcode this, try to have some detection or at least a constant
+    m_reportedCollections.remove(separatorCharacter() + QLatin1String("[Gmail]"));
+}
+
diff --git a/resources/gmail/gmailretrievecollectionstask.h b/resources/gmail/gmailretrievecollectionstask.h
new file mode 100644 (file)
index 0000000..9e553ee
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+#ifndef GMAILRETRIEVECOLLECTIONSTASK_H
+#define GMAILRETRIEVECOLLECTIONSTASK_H
+
+#include <imap/retrievecollectionstask.h>
+
+class GmailRetrieveCollectionsTask : public RetrieveCollectionsTask
+{
+    Q_OBJECT
+public:
+    explicit GmailRetrieveCollectionsTask(ResourceStateInterface::Ptr resource, QObject *parent = 0);
+
+protected Q_SLOTS:
+    void onMailBoxesReceived(const QList<KIMAP::MailBoxDescriptor> &descriptors, const QList<QList<QByteArray> > &flags);
+
+};
+
+#endif // GMAILRETRIEVECOLLECTIONSTASK_H
diff --git a/resources/gmail/gmailretrieveitemstask.cpp b/resources/gmail/gmailretrieveitemstask.cpp
new file mode 100644 (file)
index 0000000..6a15540
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+#include "gmailretrieveitemstask.h"
+#include "gmailresourcestate.h"
+
+#include <imap/batchfetcher.h>
+
+#include <AkonadiCore/CollectionFetchJob>
+#include <AkonadiCore/LinkJob>
+
+GmailRetrieveItemsTask::GmailRetrieveItemsTask(ResourceStateInterface::Ptr resource, QObject *parent)
+    : RetrieveItemsTask(resource, parent)
+{
+    qDebug();
+    dynamic_cast<GmailResourceState *>(resource.get())->setCurrentTask(this);
+}
+
+GmailRetrieveItemsTask::~GmailRetrieveItemsTask()
+{
+
+}
+
+BatchFetcher *GmailRetrieveItemsTask::createBatchFetcher(MessageHelper::Ptr messageHelper,
+        const KIMAP::ImapSet &set,
+        const KIMAP::FetchJob::FetchScope &scope,
+        int batchSize,
+        KIMAP::Session *session)
+{
+    qDebug();
+    KIMAP::FetchJob::FetchScope gmailScope = scope;
+    BatchFetcher *batchFetcher = new BatchFetcher(messageHelper, set, gmailScope, batchSize, session);
+    batchFetcher->setGmailExtensionsEnabled(true);
+    return batchFetcher;
+}
+
+bool GmailRetrieveItemsTask::serverSupportsCondstore() const
+{
+    return true;
+}
diff --git a/resources/gmail/gmailretrieveitemstask.h b/resources/gmail/gmailretrieveitemstask.h
new file mode 100644 (file)
index 0000000..642f9dd
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+#ifndef GMAILRETRIEVEITEMSTASK_H
+#define GMAILRETRIEVEITEMSTASK_H
+
+#include <imap/retrieveitemstask.h>
+
+#include <KImap/FetchJob>
+
+class GmailRetrieveItemsTask : public RetrieveItemsTask
+{
+    Q_OBJECT
+public:
+    explicit GmailRetrieveItemsTask(ResourceStateInterface::Ptr resource, QObject *parent = 0);
+    ~GmailRetrieveItemsTask();
+
+    virtual bool serverSupportsCondstore() const;
+
+Q_SIGNALS:
+    void linkItem(const QString &remoteId, const QVector<QByteArray> &labels);
+
+protected:
+    BatchFetcher *createBatchFetcher(MessageHelper::Ptr messageHelper,
+                                     const KIMAP::ImapSet &set,
+                                     const KIMAP::FetchJob::FetchScope &scope,
+                                     int batchSize,
+                                     KIMAP::Session *session);
+
+private:
+    // Allow GmailMessageHelper to emit linkItem() for us
+    friend class GmailMessageHelper;
+};
+
+#endif // GMAILRETRIEVEITEMSTASK_H
diff --git a/resources/gmail/gmailsettings.cpp b/resources/gmail/gmailsettings.cpp
new file mode 100644 (file)
index 0000000..d5b1622
--- /dev/null
@@ -0,0 +1,300 @@
+/*
+ * <one line to give the library's name and an idea of what it does.>
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#include "gmailsettings.h"
+//#include "settingsadaptor.h"
+
+#include "imapaccount.h"
+
+#include <kwallet.h>
+using KWallet::Wallet;
+#include <KLocalizedString>
+#include <kpassworddialog.h>
+
+#include <QDBusConnection>
+
+#include <AkonadiCore/Collection>
+#include <AkonadiCore/CollectionFetchJob>
+#include <AkonadiCore/CollectionModifyJob>
+
+#include <KGAPI/Account>
+#include <KGAPI/AuthJob>
+
+GmailSettings::GmailSettings(WId winId)
+    : Settings(winId)
+    , mActiveAuthJob(0)
+{
+    // Try to initialize mAccount
+    requestAccount(false);
+    /*
+    new SettingsAdaptor( this );
+    QDBusConnection::sessionBus().registerObject( QLatin1String( "/GmailSettings" ), this,
+                                                  QDBusConnection::ExportAdaptors | QDBusConnection::ExportScriptableContents );
+    */
+}
+
+QString GmailSettings::apiKey() const
+{
+    return QLatin1String("554041944266.apps.googleusercontent.com");
+}
+
+QString GmailSettings::secretKey() const
+{
+    return QLatin1String("mdT1DjzohxN3npUUzkENT0gO");
+}
+
+void GmailSettings::clearCachedPassword()
+{
+    mAccount = KGAPI2::AccountPtr();
+}
+
+void GmailSettings::cleanup()
+{
+    Wallet *wallet = Wallet::openWallet(Wallet::NetworkWallet(), m_winId);
+    if (wallet && wallet->isOpen()) {
+        if (wallet->hasFolder(QLatin1String("gmail"))) {
+            wallet->setFolder(QLatin1String("gmail"));
+            wallet->removeEntry(config()->name());
+        }
+        delete wallet;
+    }
+}
+
+void GmailSettings::requestPassword()
+{
+    requestAccount(false);
+    if (mAccount) {
+        Q_EMIT passwordRequestCompleted(mAccount->accessToken(), false);
+    } else {
+        Q_EMIT passwordRequestCompleted(QString(), false);
+    }
+}
+
+void GmailSettings::requestAccount(bool authenticate)
+{
+    bool userRejected = false;
+    loadAccountFromKWallet(&userRejected);
+    if (userRejected) {
+        Q_EMIT accountRequestCompleted(KGAPI2::AccountPtr(), true);
+        return;
+    }
+
+    if (authenticate) {
+        if (mActiveAuthJob) {
+            return;
+        }
+
+        if (!mAccount) {
+            mAccount = KGAPI2::AccountPtr(new KGAPI2::Account());
+            mAccount->addScope(QUrl(QLatin1String("https://mail.google.com")));
+        }
+
+        KGAPI2::AuthJob *authJob = new KGAPI2::AuthJob(mAccount, apiKey(), secretKey(), this);
+        connect(authJob, &KGAPI2::Job::finished,
+                this, &GmailSettings::onAuthFinished);
+        mActiveAuthJob = authJob;
+    } else {
+        if (mAccount) {
+            setImapServer(QLatin1String("imap.gmail.com"));
+        } else {
+            setImapServer(QString());
+        }
+        Q_EMIT accountRequestCompleted(mAccount, false);
+    }
+}
+
+void GmailSettings::onAuthFinished(KGAPI2::Job *job)
+{
+    mActiveAuthJob = 0;
+
+    if (job->error()) {
+        Q_EMIT accountRequestCompleted(KGAPI2::AccountPtr(), job->error() == KGAPI2::AuthCancelled);
+        return;
+    }
+
+    KGAPI2::AuthJob *auth = qobject_cast<KGAPI2::AuthJob *>(job);
+    const KGAPI2::AccountPtr account = auth->account();
+    storeAccount(account);
+    setImapServer(QLatin1String("imap.gmail.com"));
+
+    Q_EMIT accountRequestCompleted(account, false);
+}
+
+void GmailSettings::onWalletOpened(bool success)
+{
+    if (!success) {
+        emit passwordRequestCompleted(QString(), true);
+    } else {
+        Wallet *wallet = qobject_cast<Wallet *>(sender());
+        bool passwordNotStoredInWallet = true;
+        if (wallet && wallet->hasFolder(QLatin1String("gmail"))) {
+            loadAccountFromKWallet();
+            passwordNotStoredInWallet = false;
+        }
+        if (passwordNotStoredInWallet || !mAccount) {
+            /* FIXME: Manual auth */
+            //requestManualAuth();
+        } else {
+            emit passwordRequestCompleted(mAccount->accessToken(), passwordNotStoredInWallet);
+        }
+
+        if (wallet) {
+            wallet->deleteLater();
+        }
+    }
+}
+
+void GmailSettings::loadAccountFromKWallet(bool *userRejected) const
+{
+    if (userRejected != 0) {
+        *userRejected = false;
+    }
+
+    Wallet *wallet = Wallet::openWallet(Wallet::NetworkWallet(), m_winId);
+    if (wallet && wallet->isOpen()) {
+        if (wallet->hasFolder(QLatin1String("gmail"))) {
+            wallet->setFolder(QLatin1String("gmail"));
+            QMap<QString, QString> map;
+            wallet->readMap(config()->name(), map);
+            mAccount = KGAPI2::AccountPtr(new KGAPI2::Account(map[QLatin1String("accountName")],
+                                          map[QLatin1String("accessToken")],
+                                          map[QLatin1String("refreshToken")],
+                                          QList<QUrl>() << QUrl(QLatin1String("https://mail.google.com"))
+                                          << KGAPI2::Account::accountInfoScopeUrl()
+                                          << KGAPI2::Account::accountInfoEmailScopeUrl()));
+        } else {
+            wallet->createFolder(QLatin1String("gmail"));
+            mAccount = KGAPI2::AccountPtr();
+        }
+    } else {
+        mAccount = KGAPI2::AccountPtr();
+        if (userRejected != 0) {
+            *userRejected = true;
+        }
+    }
+}
+
+void GmailSettings::saveAccountToKWallet()
+{
+    Wallet *wallet = Wallet::openWallet(Wallet::NetworkWallet(), m_winId);
+    if (wallet && wallet->isOpen()) {
+        if (!wallet->hasFolder(QLatin1String("gmail"))) {
+            wallet->createFolder(QLatin1String("gmail"));
+        }
+        wallet->setFolder(QLatin1String("gmail"));
+        QMap<QString, QString> map;
+        map[QLatin1String("accountName")] = mAccount->accountName();
+        map[QLatin1String("accessToken")] = mAccount->accessToken();
+        map[QLatin1String("refreshToken")] = mAccount->refreshToken();
+        wallet->writeMap(config()->name(), map);
+        qDebug() << "Wallet save: " << wallet->sync();
+    }
+    delete wallet;
+}
+
+QString GmailSettings::accountName(bool *userRejected) const
+{
+    if (!mAccount) {
+        loadAccountFromKWallet(userRejected);
+    }
+
+    return mAccount->accountName();
+}
+
+void GmailSettings::setAccountName(const QString &accountName)
+{
+    if (accountName == mAccount->accountName()) {
+        return;
+    }
+
+    mAccount->setAccountName(accountName);
+    saveAccountToKWallet();
+}
+
+QString GmailSettings::password(bool *userRejected) const
+{
+    if (!mAccount) {
+        loadAccountFromKWallet(userRejected);
+    }
+    if (mAccount) {
+        return mAccount->accessToken();
+    }
+    return QString();
+}
+
+void GmailSettings::setPassword(const QString &accessToken)
+{
+    if (accessToken == mAccount->accessToken()) {
+        return;
+    }
+
+    mAccount->setAccessToken(accessToken);
+    saveAccountToKWallet();
+}
+
+QString GmailSettings::refreshToken(bool *userRejected) const
+{
+    if (!mAccount) {
+        loadAccountFromKWallet(userRejected);
+    }
+
+    return mAccount->refreshToken();
+}
+
+void GmailSettings::setRefreshToken(const QString &refreshToken)
+{
+    if (refreshToken == mAccount->refreshToken()) {
+        return;
+    }
+
+    mAccount->setRefreshToken(refreshToken);
+    saveAccountToKWallet();
+}
+
+void GmailSettings::loadAccount(ImapAccount *account) const
+{
+    qDebug() << userName();
+    account->setServer(QLatin1String("imap.gmail.com"));
+    account->setPort(993);
+
+    account->setUserName(userName());
+    account->setSubscriptionEnabled(subscriptionEnabled());
+
+    account->setEncryptionMode(KIMAP::LoginJob::SslV3);
+    account->setAuthenticationMode(KIMAP::LoginJob::XOAuth2);
+
+    account->setTimeout(sessionTimeout());
+}
+
+void GmailSettings::storeAccount(const KGAPI2::AccountPtr &account)
+{
+    if (!account) {
+        cleanup();
+        return;
+    }
+
+    mAccount = account;
+    saveAccountToKWallet();
+}
+
+QString GmailSettings::rootRemoteId() const
+{
+    return QLatin1String("imap://") + userName() + QLatin1Char('@') + imapServer() + QLatin1Char('/');
+}
diff --git a/resources/gmail/gmailsettings.h b/resources/gmail/gmailsettings.h
new file mode 100644 (file)
index 0000000..16716c1
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2014  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+#ifndef GMAILSETTINGS_H
+#define GMAILSETTINGS_H
+
+#include <imap/settings.h>
+
+#include <KGAPI/Types>
+
+namespace KGAPI2
+{
+class Job;
+class AuthJob;
+}
+
+class ImapAccount;
+class KJob;
+
+class GmailSettings : public Settings
+{
+    Q_OBJECT
+
+public:
+    explicit GmailSettings(WId wid = 0);
+
+    void requestPassword();
+    void requestAccount(bool authenticate = false);
+
+    void loadAccount(ImapAccount *account) const;
+    void storeAccount(const KGAPI2::AccountPtr &account);
+
+    /* FIXME: I have serious doubts about this methods...they should be in the
+     * Resource, not here. */
+    QString rootRemoteId() const;
+
+    // Actually cleans tokens
+    void clearCachedPassword();
+    void cleanup();
+
+    QString apiKey() const;
+    QString secretKey() const;
+
+Q_SIGNALS:
+    void passwordRequestCompleted(const QString &password, bool userRejected);
+    void accountRequestCompleted(const KGAPI2::AccountPtr &account, bool userRejected);
+
+public Q_SLOTS:
+    Q_SCRIPTABLE QString accountName(bool *userRejected = 0) const;
+    Q_SCRIPTABLE void setAccountName(const QString &accountName);
+
+    Q_SCRIPTABLE QString password(bool *userRejected = 0) const;
+    Q_SCRIPTABLE void setPassword(const QString &accessToken);
+
+    Q_SCRIPTABLE QString refreshToken(bool *userRejected = 0) const;
+    Q_SCRIPTABLE void setRefreshToken(const QString &refreshToken);
+
+private Q_SLOTS:
+    void onWalletOpened(bool success);
+
+    void loadAccountFromKWallet(bool *userRejected = 0) const;
+    void saveAccountToKWallet();
+
+    void onAuthFinished(KGAPI2::Job *job);
+
+private:
+    mutable KGAPI2::AccountPtr mAccount;
+    KGAPI2::AuthJob *mActiveAuthJob;
+
+};
+
+#endif
diff --git a/resources/gmail/saslplugin/CMakeLists.txt b/resources/gmail/saslplugin/CMakeLists.txt
new file mode 100644 (file)
index 0000000..5617b73
--- /dev/null
@@ -0,0 +1,23 @@
+find_package(Sasl2)
+set_package_properties(Sasl2 PROPERTIES DESCRIPTION "cyrus-sasl" URL "http://asg.web.cmu.edu/sasl/sasl-library.html" TYPE REQUIRED PURPOSE "Login authentication for Gmail resource")
+
+add_definitions(-D_POSIX_SOURCE)
+
+include_directories(${Sasl2_INCLUDE_DIRS}
+                    ${Sasl2_INCLUDE_DIR}/sasl)
+
+set(kdexoauth2sasl_SRCS
+    plugin_common.c
+    xoauth2plugin.c
+    xoauth2plugin_init.c
+)
+
+if(Sasl2_FOUND)
+  set(kdexoauth2sasl_EXTRA_LIBS ${Sasl2_LIBRARIES})
+endif()
+
+
+add_library(kdexoauth2 SHARED ${kdexoauth2sasl_SRCS} ${kdexoauth2sasl_EXTRA_LIBS})
+set_target_properties(kdexoauth2 PROPERTIES SOVERSION 4 VERSION 4.0.0)
+
+install(TARGETS kdexoauth2 DESTINATION ${KDE_INSTALL_LIBDIR}/sasl2)
diff --git a/resources/gmail/saslplugin/config.h b/resources/gmail/saslplugin/config.h
new file mode 100644 (file)
index 0000000..266ea3e
--- /dev/null
@@ -0,0 +1,579 @@
+/* config.h.  Generated by configure.  */
+/* config.h.in.  Generated from configure.in by autoheader.  */
+
+/* acconfig.h - autoheader configuration input */
+/*
+ * Copyright (c) 1998-2003 Carnegie Mellon University.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the
+ *    distribution.
+ *
+ * 3. The name "Carnegie Mellon University" must not be used to
+ *    endorse or promote products derived from this software without
+ *    prior written permission. For permission or any other legal
+ *    details, please contact
+ *      Office of Technology Transfer
+ *      Carnegie Mellon University
+ *      5000 Forbes Avenue
+ *      Pittsburgh, PA  15213-3890
+ *      (412) 268-4387, fax: (412) 268-7395
+ *      tech-transfer@andrew.cmu.edu
+ *
+ * 4. Redistributions of any form whatsoever must retain the following
+ *    acknowledgment:
+ *    "This product includes software developed by Computing Services
+ *     at Carnegie Mellon University (http://www.cmu.edu/computing/)."
+ *
+ * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
+ * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
+ * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
+ * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef CONFIG_H
+#define CONFIG_H
+
+/* Runtime config file location */
+#define CONFIGDIR "/usr/lib/sasl2:/etc/sasl2"
+
+/* Do we need a leading _ for dlsym? */
+/* #undef DLSYM_NEEDS_UNDERSCORE */
+
+/* Should we build a shared plugin (via dlopen) library? */
+#define DO_DLOPEN
+
+/* should we support sasl_checkapop? */
+#define DO_SASL_CHECKAPOP
+
+/* should we support setpass() for SRP? */
+/* #undef DO_SRP_SETPASS */
+
+/* should we mutex-wrap calls into the GSS library? */
+#define GSS_USE_MUTEXES
+
+/* Enable 'alwaystrue' password verifier? */
+/* #undef HAVE_ALWAYSTRUE */
+
+/* Include support for Courier's authdaemond? */
+#define HAVE_AUTHDAEMON
+
+/* Define to 1 if you have the <des.h> header file. */
+/* #undef HAVE_DES_H */
+
+/* Define to 1 if you have the <dirent.h> header file, and it defines `DIR'.
+   */
+#define HAVE_DIRENT_H 1
+
+/* Define to 1 if you have the <dlfcn.h> header file. */
+#define HAVE_DLFCN_H 1
+
+/* Define to 1 if you have the `dns_lookup' function. */
+/* #undef HAVE_DNS_LOOKUP */
+
+/* Define to 1 if you have the `dn_expand' function. */
+#define HAVE_DN_EXPAND 1
+
+/* Define to 1 if you have the <fcntl.h> header file. */
+#define HAVE_FCNTL_H 1
+
+/* Do we have a getaddrinfo? */
+#define HAVE_GETADDRINFO
+
+/* Define to 1 if you have the `getdomainname' function. */
+#define HAVE_GETDOMAINNAME 1
+
+/* Define to 1 if you have the `gethostname' function. */
+#define HAVE_GETHOSTNAME 1
+
+/* Do we have a getnameinfo() function? */
+#define HAVE_GETNAMEINFO
+
+/* Define to 1 if you have the `getpwnam' function. */
+#define HAVE_GETPWNAM 1
+
+/* Define to 1 if you have the `getspnam' function. */
+#define HAVE_GETSPNAM 1
+
+/* do we have getsubopt()? */
+#define HAVE_GETSUBOPT
+
+/* Define to 1 if you have the `gettimeofday' function. */
+#define HAVE_GETTIMEOFDAY 1
+
+/* Define if you have the gssapi.h header file */
+#define HAVE_GSSAPI_H
+
+/* Define to 1 if you have the `gsskrb5_register_acceptor_identity' function.
+   */
+/* #undef HAVE_GSSKRB5_REGISTER_ACCEPTOR_IDENTITY */
+
+/* Define if your GSSAPI implimentation defines GSS_C_NT_HOSTBASED_SERVICE */
+#define HAVE_GSS_C_NT_HOSTBASED_SERVICE
+
+/* Define if your GSSAPI implimentation defines GSS_C_NT_USER_NAME */
+#define HAVE_GSS_C_NT_USER_NAME
+
+/* Define to 1 if you have the `inet_aton' function. */
+#define HAVE_INET_ATON 1
+
+/* Define to 1 if you have the <inttypes.h> header file. */
+#define HAVE_INTTYPES_H 1
+
+/* Define to 1 if you have the `jrand48' function. */
+#define HAVE_JRAND48 1
+
+/* Do we have Kerberos 4 Support? */
+/* #undef HAVE_KRB */
+
+/* Define to 1 if you have the `krb_get_err_text' function. */
+/* #undef HAVE_KRB_GET_ERR_TEXT */
+
+/* Define to 1 if you have the <lber.h> header file. */
+/* #undef HAVE_LBER_H */
+
+/* Define to 1 if you have the <ldap.h> header file. */
+/* #undef HAVE_LDAP_H */
+
+/* Define to 1 if you have the `resolv' library (-lresolv). */
+#define HAVE_LIBRESOLV 1
+
+/* Define to 1 if you have the <limits.h> header file. */
+#define HAVE_LIMITS_H 1
+
+/* Define to 1 if you have the <malloc.h> header file. */
+#define HAVE_MALLOC_H 1
+
+/* Define to 1 if you have the `memcpy' function. */
+#define HAVE_MEMCPY 1
+
+/* Define to 1 if you have the <memory.h> header file. */
+#define HAVE_MEMORY_H 1
+
+/* Define to 1 if you have the `mkdir' function. */
+#define HAVE_MKDIR 1
+
+/* Do we have mysql support? */
+/* #undef HAVE_MYSQL */
+
+/* Define to 1 if you have the <ndir.h> header file, and it defines `DIR'. */
+/* #undef HAVE_NDIR_H */
+
+/* Do we have OpenSSL? */
+#define HAVE_OPENSSL
+
+/* Use OPIE for server-side OTP? */
+/* #undef HAVE_OPIE */
+
+/* Define to 1 if you have the <pam/pam_appl.h> header file. */
+/* #undef HAVE_PAM_PAM_APPL_H */
+
+/* Define to 1 if you have the <paths.h> header file. */
+#define HAVE_PATHS_H 1
+
+/* Do we have Postgres support? */
+/* #undef HAVE_PGSQL */
+
+/* Include Support for pwcheck daemon? */
+/* #undef HAVE_PWCHECK */
+
+/* Include support for saslauthd? */
+#define HAVE_SASLAUTHD
+
+/* Define to 1 if you have the <security/pam_appl.h> header file. */
+#define HAVE_SECURITY_PAM_APPL_H 1
+
+/* Define to 1 if you have the `select' function. */
+#define HAVE_SELECT 1
+
+/* Does the system have snprintf()? */
+#define HAVE_SNPRINTF
+
+/* Does sockaddr have an sa_len? */
+/* #undef HAVE_SOCKADDR_SA_LEN */
+
+/* Define to 1 if you have the `socket' function. */
+#define HAVE_SOCKET 1
+
+/* Do we have a socklen_t? */
+#define HAVE_SOCKLEN_T
+
+/* Do we have SQLite support? */
+/* #undef HAVE_SQLITE */
+
+/* Is there an ss_family in sockaddr_storage? */
+#define HAVE_SS_FAMILY
+
+/* Define to 1 if you have the <stdarg.h> header file. */
+#define HAVE_STDARG_H 1
+
+/* Define to 1 if you have the <stdint.h> header file. */
+#define HAVE_STDINT_H 1
+
+/* Define to 1 if you have the <stdlib.h> header file. */
+#define HAVE_STDLIB_H 1
+
+/* Define to 1 if you have the `strchr' function. */
+#define HAVE_STRCHR 1
+
+/* Define to 1 if you have the `strdup' function. */
+#define HAVE_STRDUP 1
+
+/* Define to 1 if you have the `strerror' function. */
+#define HAVE_STRERROR 1
+
+/* Define to 1 if you have the <strings.h> header file. */
+#define HAVE_STRINGS_H 1
+
+/* Define to 1 if you have the <string.h> header file. */
+#define HAVE_STRING_H 1
+
+/* Define to 1 if you have the `strspn' function. */
+#define HAVE_STRSPN 1
+
+/* Define to 1 if you have the `strstr' function. */
+#define HAVE_STRSTR 1
+
+/* Define to 1 if you have the `strtol' function. */
+#define HAVE_STRTOL 1
+
+/* Do we have struct sockaddr_stroage? */
+#define HAVE_STRUCT_SOCKADDR_STORAGE
+
+/* Define to 1 if you have the <sysexits.h> header file. */
+#define HAVE_SYSEXITS_H 1
+
+/* Define to 1 if you have the `syslog' function. */
+#define HAVE_SYSLOG 1
+
+/* Define to 1 if you have the <syslog.h> header file. */
+#define HAVE_SYSLOG_H 1
+
+/* Define to 1 if you have the <sys/dir.h> header file, and it defines `DIR'.
+   */
+/* #undef HAVE_SYS_DIR_H */
+
+/* Define to 1 if you have the <sys/file.h> header file. */
+#define HAVE_SYS_FILE_H 1
+
+/* Define to 1 if you have the <sys/ndir.h> header file, and it defines `DIR'.
+   */
+/* #undef HAVE_SYS_NDIR_H */
+
+/* Define to 1 if you have the <sys/param.h> header file. */
+#define HAVE_SYS_PARAM_H 1
+
+/* Define to 1 if you have the <sys/stat.h> header file. */
+#define HAVE_SYS_STAT_H 1
+
+/* Define to 1 if you have the <sys/time.h> header file. */
+#define HAVE_SYS_TIME_H 1
+
+/* Define to 1 if you have the <sys/types.h> header file. */
+#define HAVE_SYS_TYPES_H 1
+
+/* Define to 1 if you have the <sys/uio.h> header file. */
+#define HAVE_SYS_UIO_H 1
+
+/* Define to 1 if you have <sys/wait.h> that is POSIX.1 compatible. */
+#define HAVE_SYS_WAIT_H 1
+
+/* Define to 1 if you have the <unistd.h> header file. */
+#define HAVE_UNISTD_H 1
+
+/* Define to 1 if you have the <varargs.h> header file. */
+/* #undef HAVE_VARARGS_H */
+
+/* Does the system have vsnprintf()? */
+#define HAVE_VSNPRINTF
+
+/* define if your compiler has __attribute__ */
+/* #undef HAVE___ATTRIBUTE__ */
+
+/* Should we keep handle to Berkeley DB open in SASLDB plugin? */
+/* #undef KEEP_DB_OPEN */
+
+/* Ignore IP Address in Kerberos 4 tickets? */
+/* #undef KRB4_IGNORE_IP_ADDRESS */
+
+/* Name of package */
+#define PACKAGE "cyrus-sasl"
+
+/* Define to the address where bug reports for this package should be sent. */
+#define PACKAGE_BUGREPORT ""
+
+/* Define to the full name of this package. */
+#define PACKAGE_NAME ""
+
+/* Define to the full name and version of this package. */
+#define PACKAGE_STRING ""
+
+/* Define to the one symbol short name of this package. */
+#define PACKAGE_TARNAME ""
+
+/* Define to the version of this package. */
+#define PACKAGE_VERSION ""
+
+/* Where do we look for Courier authdaemond's socket? */
+#define PATH_AUTHDAEMON_SOCKET "/dev/null"
+
+/* Where do we look for saslauthd's socket? */
+#define PATH_SASLAUTHD_RUNDIR "/var/state/saslauthd"
+
+/* Runtime plugin location */
+#define PLUGINDIR "/usr/lib/sasl2"
+
+/* Force a preferred mechanism */
+/* #undef PREFER_MECH */
+
+/* Location of pwcheck socket */
+/* #undef PWCHECKDIR */
+
+/* Define as the return type of signal handlers (`int' or `void'). */
+#define RETSIGTYPE void
+
+/* Use BerkeleyDB for SASLdb */
+#define SASL_BERKELEYDB
+
+/* Path to default SASLdb database */
+#define SASL_DB_PATH "/etc/sasldb2"
+
+/* File to use for source of randomness */
+#define SASL_DEV_RANDOM "/dev/random"
+
+/* Use GDBM for SASLdb */
+/* #undef SASL_GDBM */
+
+/* Use NDBM for SASLdb */
+/* #undef SASL_NDBM */
+
+/* The size of a `long', as computed by sizeof. */
+#define SIZEOF_LONG 4
+
+/* Link ANONYMOUS Staticly */
+/* #undef STATIC_ANONYMOUS */
+
+/* Link CRAM-MD5 Staticly */
+/* #undef STATIC_CRAMMD5 */
+
+/* Link DIGEST-MD5 Staticly */
+/* #undef STATIC_DIGESTMD5 */
+
+/* Link GSSAPI Staticly */
+#define STATIC_GSSAPIV2
+
+/* User KERBEROS_V4 Staticly */
+/* #undef STATIC_KERBEROS4 */
+
+/* Link ldapdb plugin Staticly */
+/* #undef STATIC_LDAPDB */
+
+/* Link LOGIN Staticly */
+/* #undef STATIC_LOGIN */
+
+/* Link NTLM Staticly */
+/* #undef STATIC_NTLM */
+
+/* Link OTP Staticly */
+/* #undef STATIC_OTP */
+
+/* Link PASSDSS Staticly */
+/* #undef STATIC_PASSDSS */
+
+/* Link PLAIN Staticly */
+/* #undef STATIC_PLAIN */
+
+/* Link OAUTH Staticly */
+/* #undef STATIC_OAUTH */
+
+/* Link SASLdb Staticly */
+/* #undef STATIC_SASLDB */
+
+/* Link SQL plugin staticly */
+/* #undef STATIC_SQL */
+
+/* Link SRP Staticly */
+/* #undef STATIC_SRP */
+
+/* Define to 1 if you have the ANSI C header files. */
+#define STDC_HEADERS 1
+
+/* Define to 1 if you can safely include both <sys/time.h> and <time.h>. */
+#define TIME_WITH_SYS_TIME 1
+
+/* Should we try to dlopen() plugins while staticly compiled? */
+/* #undef TRY_DLOPEN_WHEN_STATIC */
+
+/* use the doors IPC API for saslauthd? */
+/* #undef USE_DOORS */
+
+/* Version number of package */
+#define VERSION "2.1.23"
+
+/* Use DES */
+#define WITH_DES
+
+/* Linking against dmalloc? */
+/* #undef WITH_DMALLOC */
+
+/* Use internal RC4 implementation? */
+#define WITH_RC4
+
+/* Use OpenSSL DES Implementation */
+#define WITH_SSL_DES
+
+/* Define to empty if `const' does not conform to ANSI C. */
+/* #undef const */
+
+/* Define as `__inline' if that's what the C compiler calls it, or to nothing
+   if it is not supported. */
+/* #undef inline */
+
+/* Define to `int' if <sys/types.h> does not define. */
+/* #undef mode_t */
+
+/* Define to `int' if <sys/types.h> does not define. */
+/* #undef pid_t */
+
+/* Create a struct iovec if we need one */
+#if !defined(_WIN32) && !defined(HAVE_SYS_UIO_H)
+/* (win32 is handled in sasl.h) */
+struct iovec {
+    char *iov_base;
+    long iov_len;
+};
+#else
+#include <sys/types.h>
+#include <sys/uio.h>
+#endif
+
+/* location of the random number generator */
+#ifdef DEV_RANDOM
+/* #undef DEV_RANDOM */
+#endif
+#define DEV_RANDOM SASL_DEV_RANDOM
+
+/* if we've got krb_get_err_txt, we might as well use it;
+   especially since krb_err_txt isn't in some newer distributions
+   (MIT Kerb for Mac 4 being a notable example). If we don't have
+   it, we fall back to the krb_err_txt array */
+#ifdef HAVE_KRB_GET_ERR_TEXT
+#define get_krb_err_txt krb_get_err_text
+#else
+#define get_krb_err_txt(X) (krb_err_txt[(X)])
+#endif
+
+/* Make Solaris happy... */
+#ifndef __EXTENSIONS__
+#define __EXTENSIONS__
+#endif
+
+/* Make Linux happy... */
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#ifndef HAVE___ATTRIBUTE__
+/* Can't use attributes... */
+#define __attribute__(foo)
+#endif
+
+#define SASL_PATH_ENV_VAR "SASL_PATH"
+#define SASL_CONF_PATH_ENV_VAR "SASL_CONF_PATH"
+
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#ifndef WIN32
+# include <netdb.h>
+# ifdef HAVE_SYS_PARAM_H
+#  include <sys/param.h>
+# endif
+#else /* WIN32 */
+# include <winsock2.h>
+#endif /* WIN32 */
+#include <string.h>
+
+#include <netinet/in.h>
+
+#ifndef HAVE_SOCKLEN_T
+typedef unsigned int socklen_t;
+#endif /* HAVE_SOCKLEN_T */
+
+#ifndef HAVE_STRUCT_SOCKADDR_STORAGE
+#define _SS_MAXSIZE 128 /* Implementation specific max size */
+#define _SS_PADSIZE (_SS_MAXSIZE - sizeof (struct sockaddr))
+
+struct sockaddr_storage {
+    struct  sockaddr ss_sa;
+    char        __ss_pad2[_SS_PADSIZE];
+};
+# define ss_family ss_sa.sa_family
+#endif /* !HAVE_STRUCT_SOCKADDR_STORAGE */
+
+#ifndef AF_INET6
+/* Define it to something that should never appear */
+#define AF_INET6    AF_MAX
+#endif
+
+#ifndef HAVE_GETADDRINFO
+#define getaddrinfo sasl_getaddrinfo
+#define freeaddrinfo    sasl_freeaddrinfo
+#define gai_strerror    sasl_gai_strerror
+#endif
+
+#ifndef HAVE_GETNAMEINFO
+#define getnameinfo sasl_getnameinfo
+#endif
+
+#if !defined(HAVE_GETNAMEINFO) || !defined(HAVE_GETADDRINFO)
+#include "gai.h"
+#endif
+
+#ifndef AI_NUMERICHOST   /* support glibc 2.0.x */
+#define AI_NUMERICHOST  4
+#define NI_NUMERICHOST  2
+#define NI_NAMEREQD     4
+#define NI_NUMERICSERV  8
+#endif
+
+/* Defined in RFC 1035. max strlen is only 253 due to length bytes. */
+#ifndef MAXHOSTNAMELEN
+#define        MAXHOSTNAMELEN  255
+#endif
+
+#ifndef HAVE_SYSEXITS_H
+#include "exits.h"
+#else
+#include "sysexits.h"
+#endif
+
+/* Get the correct time.h */
+#if TIME_WITH_SYS_TIME
+# include <sys/time.h>
+# include <time.h>
+#else
+# if HAVE_SYS_TIME_H
+#  include <sys/time.h>
+# else
+#  include <time.h>
+# endif
+#endif
+
+#ifndef HIER_DELIMITER
+#define HIER_DELIMITER '/'
+#endif
+
+#endif /* CONFIG_H */
+
diff --git a/resources/gmail/saslplugin/plugin_common.c b/resources/gmail/saslplugin/plugin_common.c
new file mode 100644 (file)
index 0000000..f60be46
--- /dev/null
@@ -0,0 +1,969 @@
+/* Generic SASL plugin utility functions
+ * Rob Siemborski
+ * $Id: plugin_common.c,v 1.22 2011/09/01 14:12:18 mel Exp $
+ */
+/*
+ * Copyright (c) 1998-2003 Carnegie Mellon University.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the
+ *    distribution.
+ *
+ * 3. The name "Carnegie Mellon University" must not be used to
+ *    endorse or promote products derived from this software without
+ *    prior written permission. For permission or any other legal
+ *    details, please contact
+ *      Office of Technology Transfer
+ *      Carnegie Mellon University
+ *      5000 Forbes Avenue
+ *      Pittsburgh, PA  15213-3890
+ *      (412) 268-4387, fax: (412) 268-7395
+ *      tech-transfer@andrew.cmu.edu
+ *
+ * 4. Redistributions of any form whatsoever must retain the following
+ *    acknowledgment:
+ *    "This product includes software developed by Computing Services
+ *     at Carnegie Mellon University (http://www.cmu.edu/computing/)."
+ *
+ * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
+ * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
+ * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
+ * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <config.h>
+#ifndef macintosh
+#ifdef WIN32
+# include <winsock2.h>
+#else
+# include <sys/socket.h>
+# include <sys/types.h>
+# include <netinet/in.h>
+# include <arpa/inet.h>
+# include <netdb.h>
+# include <sys/utsname.h>
+#endif /* WIN32 */
+#endif /* macintosh */
+#ifdef HAVE_UNISTD_H
+#include <unistd.h>
+#endif
+#include <fcntl.h>
+#include <sasl.h>
+#include <saslutil.h>
+#include <saslplug.h>
+
+#include <errno.h>
+#include <ctype.h>
+#include <stdio.h>
+
+#ifdef HAVE_INTTYPES_H
+#include <inttypes.h>
+#endif
+
+#include "plugin_common.h"
+
+/* translate IPv4 mapped IPv6 address to IPv4 address */
+static void sockaddr_unmapped(
+#ifdef IN6_IS_ADDR_V4MAPPED
+    struct sockaddr *sa, socklen_t *len
+#else
+    struct sockaddr *sa __attribute__((unused)),
+    socklen_t *len __attribute__((unused))
+#endif
+)
+{
+#ifdef IN6_IS_ADDR_V4MAPPED
+    struct sockaddr_in6 *sin6;
+    struct sockaddr_in *sin4;
+    uint32_t addr;
+    int port;
+
+    if (sa->sa_family != AF_INET6) {
+        return;
+    }
+    sin6 = (struct sockaddr_in6 *)sa;
+    if (!IN6_IS_ADDR_V4MAPPED((&sin6->sin6_addr))) {
+        return;
+    }
+    sin4 = (struct sockaddr_in *)sa;
+    addr = *(uint32_t *)&sin6->sin6_addr.s6_addr[12];
+    port = sin6->sin6_port;
+    memset(sin4, 0, sizeof(struct sockaddr_in));
+    sin4->sin_addr.s_addr = addr;
+    sin4->sin_port = port;
+    sin4->sin_family = AF_INET;
+#ifdef HAVE_SOCKADDR_SA_LEN
+    sin4->sin_len = sizeof(struct sockaddr_in);
+#endif
+    *len = sizeof(struct sockaddr_in);
+#else
+    return;
+#endif
+}
+
+int _plug_ipfromstring(const sasl_utils_t *utils, const char *addr,
+                       struct sockaddr *out, socklen_t outlen)
+{
+    int i, j;
+    socklen_t len;
+    struct sockaddr_storage ss;
+    struct addrinfo hints, *ai = NULL;
+    char hbuf[NI_MAXHOST];
+
+    if (!utils || !addr || !out) {
+        if (utils) {
+            PARAMERROR(utils);
+        }
+        return SASL_BADPARAM;
+    }
+
+    /* Parse the address */
+    for (i = 0; addr[i] != '\0' && addr[i] != ';'; i++) {
+        if (i >= NI_MAXHOST) {
+            if (utils) {
+                PARAMERROR(utils);
+            }
+            return SASL_BADPARAM;
+        }
+        hbuf[i] = addr[i];
+    }
+    hbuf[i] = '\0';
+
+    if (addr[i] == ';') {
+        i++;
+    }
+    /* XXX/FIXME: Do we need this check? */
+    for (j = i; addr[j] != '\0'; j++)
+        if (!isdigit((int)(addr[j]))) {
+            PARAMERROR(utils);
+            return SASL_BADPARAM;
+        }
+
+    memset(&hints, 0, sizeof(hints));
+    hints.ai_family = PF_UNSPEC;
+    hints.ai_socktype = SOCK_STREAM;
+    hints.ai_flags = AI_PASSIVE | AI_NUMERICHOST;
+
+    if (getaddrinfo(hbuf, &addr[i], &hints, &ai) != 0) {
+        PARAMERROR(utils);
+        return SASL_BADPARAM;
+    }
+
+    len = ai->ai_addrlen;
+    memcpy(&ss, ai->ai_addr, len);
+    freeaddrinfo(ai);
+    sockaddr_unmapped((struct sockaddr *)&ss, &len);
+    if (outlen < len) {
+        PARAMERROR(utils);
+        return SASL_BUFOVER;
+    }
+
+    memcpy(out, &ss, len);
+
+    return SASL_OK;
+}
+
+int _plug_iovec_to_buf(const sasl_utils_t *utils, const struct iovec *vec,
+                       unsigned numiov, buffer_info_t **output)
+{
+    unsigned i;
+    int ret;
+    buffer_info_t *out;
+    char *pos;
+
+    if (!utils || !vec || !output) {
+        if (utils) {
+            PARAMERROR(utils);
+        }
+        return SASL_BADPARAM;
+    }
+
+    if (!(*output)) {
+        *output = utils->malloc(sizeof(buffer_info_t));
+        if (!*output) {
+            MEMERROR(utils);
+            return SASL_NOMEM;
+        }
+        memset(*output, 0, sizeof(buffer_info_t));
+    }
+
+    out = *output;
+
+    out->curlen = 0;
+    for (i = 0; i < numiov; i++) {
+        out->curlen += vec[i].iov_len;
+    }
+
+    ret = _plug_buf_alloc(utils, &out->data, &out->reallen, out->curlen);
+
+    if (ret != SASL_OK) {
+        MEMERROR(utils);
+        return SASL_NOMEM;
+    }
+
+    memset(out->data, 0, out->reallen);
+    pos = out->data;
+
+    for (i = 0; i < numiov; i++) {
+        memcpy(pos, vec[i].iov_base, vec[i].iov_len);
+        pos += vec[i].iov_len;
+    }
+
+    return SASL_OK;
+}
+
+/* Basically a conditional call to realloc(), if we need more */
+int _plug_buf_alloc(const sasl_utils_t *utils, char **rwbuf,
+                    unsigned *curlen, unsigned newlen)
+{
+    if (!utils || !rwbuf || !curlen) {
+        PARAMERROR(utils);
+        return SASL_BADPARAM;
+    }
+
+    if (!(*rwbuf)) {
+        *rwbuf = utils->malloc(newlen);
+        if (*rwbuf == NULL) {
+            *curlen = 0;
+            MEMERROR(utils);
+            return SASL_NOMEM;
+        }
+        *curlen = newlen;
+    } else if (*rwbuf && *curlen < newlen) {
+        size_t needed = 2 * (*curlen);
+
+        while (needed < newlen) {
+            needed *= 2;
+        }
+
+        *rwbuf = utils->realloc(*rwbuf, needed);
+        if (*rwbuf == NULL) {
+            *curlen = 0;
+            MEMERROR(utils);
+            return SASL_NOMEM;
+        }
+        *curlen = needed;
+    }
+
+    return SASL_OK;
+}
+
+/* copy a string */
+int _plug_strdup(const sasl_utils_t *utils, const char *in,
+                 char **out, int *outlen)
+{
+    size_t len = strlen(in);
+
+    if (!utils || !in || !out) {
+        if (utils) {
+            PARAMERROR(utils);
+        }
+        return SASL_BADPARAM;
+    }
+
+    *out = utils->malloc(len + 1);
+    if (!*out) {
+        MEMERROR(utils);
+        return SASL_NOMEM;
+    }
+
+    strcpy((char *) *out, in);
+
+    if (outlen) {
+        *outlen = len;
+    }
+
+    return SASL_OK;
+}
+
+void _plug_free_string(const sasl_utils_t *utils, char **str)
+{
+    size_t len;
+
+    if (!utils || !str || !(*str)) {
+        return;
+    }
+
+    len = strlen(*str);
+
+    utils->erasebuffer(*str, len);
+    utils->free(*str);
+
+    *str = NULL;
+}
+
+void _plug_free_secret(const sasl_utils_t *utils, sasl_secret_t **secret)
+{
+    if (!utils || !secret || !(*secret)) {
+        return;
+    }
+
+    utils->erasebuffer((char *)(*secret)->data, (*secret)->len);
+    utils->free(*secret);
+    *secret = NULL;
+}
+
+/*
+ * Tries to find the prompt with the lookingfor id in the prompt list
+ * Returns it if found. NULL otherwise
+ */
+sasl_interact_t *_plug_find_prompt(sasl_interact_t **promptlist,
+                                   unsigned int lookingfor)
+{
+    sasl_interact_t *prompt;
+
+    if (promptlist && *promptlist) {
+        for (prompt = *promptlist; prompt->id != SASL_CB_LIST_END; ++prompt) {
+            if (prompt->id == lookingfor) {
+                return prompt;
+            }
+        }
+    }
+
+    return NULL;
+}
+
+/*
+ * Retrieve the simple string given by the callback id.
+ */
+int _plug_get_simple(const sasl_utils_t *utils, unsigned int id, int required,
+                     const char **result, sasl_interact_t **prompt_need)
+{
+
+    int ret = SASL_FAIL;
+    sasl_getsimple_t *simple_cb;
+    void *simple_context;
+    sasl_interact_t *prompt;
+
+    *result = NULL;
+
+    /* see if we were given the result in the prompt */
+    prompt = _plug_find_prompt(prompt_need, id);
+    if (prompt != NULL) {
+        /* We prompted, and got.*/
+
+        if (required && !prompt->result) {
+            SETERROR(utils, "Unexpectedly missing a prompt result");
+            return SASL_BADPARAM;
+        }
+
+        *result = prompt->result;
+        return SASL_OK;
+    }
+
+    /* Try to get the callback... */
+    ret = utils->getcallback(utils->conn, id, (sasl_callback_ft *)&simple_cb, &simple_context);
+
+    if (ret == SASL_FAIL && !required) {
+        return SASL_OK;
+    }
+
+    if (ret == SASL_OK && simple_cb) {
+        ret = simple_cb(simple_context, id, result, NULL);
+        if (ret != SASL_OK) {
+            return ret;
+        }
+
+        if (required && !*result) {
+            PARAMERROR(utils);
+            return SASL_BADPARAM;
+        }
+    }
+
+    return ret;
+}
+
+/*
+ * Retrieve the user password.
+ */
+int _plug_get_password(const sasl_utils_t *utils, sasl_secret_t **password,
+                       unsigned int *iscopy, sasl_interact_t **prompt_need)
+{
+    int ret = SASL_FAIL;
+    sasl_getsecret_t *pass_cb;
+    void *pass_context;
+    sasl_interact_t *prompt;
+
+    *password = NULL;
+    *iscopy = 0;
+
+    /* see if we were given the password in the prompt */
+    prompt = _plug_find_prompt(prompt_need, SASL_CB_PASS);
+    if (prompt != NULL) {
+        /* We prompted, and got.*/
+
+        if (!prompt->result) {
+            SETERROR(utils, "Unexpectedly missing a prompt result");
+            return SASL_BADPARAM;
+        }
+
+        /* copy what we got into a secret_t */
+        *password = (sasl_secret_t *) utils->malloc(sizeof(sasl_secret_t) +
+                    prompt->len + 1);
+        if (!*password) {
+            MEMERROR(utils);
+            return SASL_NOMEM;
+        }
+
+        (*password)->len = prompt->len;
+        memcpy((*password)->data, prompt->result, prompt->len);
+        (*password)->data[(*password)->len] = 0;
+
+        *iscopy = 1;
+
+        return SASL_OK;
+    }
+
+    /* Try to get the callback... */
+    ret = utils->getcallback(utils->conn, SASL_CB_PASS,
+                             (sasl_callback_ft *)&pass_cb, &pass_context);
+
+    if (ret == SASL_OK && pass_cb) {
+        ret = pass_cb(utils->conn, pass_context, SASL_CB_PASS, password);
+        if (ret != SASL_OK) {
+            return ret;
+        }
+
+        if (!*password) {
+            PARAMERROR(utils);
+            return SASL_BADPARAM;
+        }
+    }
+
+    return ret;
+}
+
+/*
+ * Retrieve the string given by the challenge prompt id.
+ */
+int _plug_challenge_prompt(const sasl_utils_t *utils, unsigned int id,
+                           const char *challenge, const char *promptstr,
+                           const char **result, sasl_interact_t **prompt_need)
+{
+    int ret = SASL_FAIL;
+    sasl_chalprompt_t *chalprompt_cb;
+    void *chalprompt_context;
+    sasl_interact_t *prompt;
+
+    *result = NULL;
+
+    /* see if we were given the password in the prompt */
+    prompt = _plug_find_prompt(prompt_need, id);
+    if (prompt != NULL) {
+        /* We prompted, and got.*/
+
+        if (!prompt->result) {
+            SETERROR(utils, "Unexpectedly missing a prompt result");
+            return SASL_BADPARAM;
+        }
+
+        *result = prompt->result;
+        return SASL_OK;
+    }
+
+    /* Try to get the callback... */
+    ret = utils->getcallback(utils->conn, id,
+                             (sasl_callback_ft *)&chalprompt_cb, &chalprompt_context);
+
+    if (ret == SASL_OK && chalprompt_cb) {
+        ret = chalprompt_cb(chalprompt_context, id,
+                            challenge, promptstr, NULL, result, NULL);
+        if (ret != SASL_OK) {
+            return ret;
+        }
+
+        if (!*result) {
+            PARAMERROR(utils);
+            return SASL_BADPARAM;
+        }
+    }
+
+    return ret;
+}
+
+/*
+ * Retrieve the client realm.
+ */
+int _plug_get_realm(const sasl_utils_t *utils, const char **availrealms,
+                    const char **realm, sasl_interact_t **prompt_need)
+{
+    int ret = SASL_FAIL;
+    sasl_getrealm_t *realm_cb;
+    void *realm_context;
+    sasl_interact_t *prompt;
+
+    *realm = NULL;
+
+    /* see if we were given the result in the prompt */
+    prompt = _plug_find_prompt(prompt_need, SASL_CB_GETREALM);
+    if (prompt != NULL) {
+        /* We prompted, and got.*/
+
+        if (!prompt->result) {
+            SETERROR(utils, "Unexpectedly missing a prompt result");
+            return SASL_BADPARAM;
+        }
+
+        *realm = prompt->result;
+        return SASL_OK;
+    }
+
+    /* Try to get the callback... */
+    ret = utils->getcallback(utils->conn, SASL_CB_GETREALM,
+                             (sasl_callback_ft *)&realm_cb, &realm_context);
+
+    if (ret == SASL_OK && realm_cb) {
+        ret = realm_cb(realm_context, SASL_CB_GETREALM, availrealms, realm);
+        if (ret != SASL_OK) {
+            return ret;
+        }
+
+        if (!*realm) {
+            PARAMERROR(utils);
+            return SASL_BADPARAM;
+        }
+    }
+
+    return ret;
+}
+
+/*
+ * Make the requested prompts. (prompt==NULL means we don't want it)
+ */
+int _plug_make_prompts(const sasl_utils_t *utils,
+                       sasl_interact_t **prompts_res,
+                       const char *user_prompt, const char *user_def,
+                       const char *auth_prompt, const char *auth_def,
+                       const char *pass_prompt, const char *pass_def,
+                       const char *echo_chal,
+                       const char *echo_prompt, const char *echo_def,
+                       const char *realm_chal,
+                       const char *realm_prompt, const char *realm_def)
+{
+    int num = 1;
+    int alloc_size;
+    sasl_interact_t *prompts;
+
+    if (user_prompt) {
+        num++;
+    }
+    if (auth_prompt) {
+        num++;
+    }
+    if (pass_prompt) {
+        num++;
+    }
+    if (echo_prompt) {
+        num++;
+    }
+    if (realm_prompt) {
+        num++;
+    }
+
+    if (num == 1) {
+        SETERROR(utils, "make_prompts() called with no actual prompts");
+        return SASL_FAIL;
+    }
+
+    alloc_size = sizeof(sasl_interact_t) * num;
+    prompts = utils->malloc(alloc_size);
+    if (!prompts) {
+        MEMERROR(utils);
+        return SASL_NOMEM;
+    }
+    memset(prompts, 0, alloc_size);
+
+    *prompts_res = prompts;
+
+    if (user_prompt) {
+        (prompts)->id = SASL_CB_USER;
+        (prompts)->challenge = "Authorization Name";
+        (prompts)->prompt = user_prompt;
+        (prompts)->defresult = user_def;
+
+        prompts++;
+    }
+
+    if (auth_prompt) {
+        (prompts)->id = SASL_CB_AUTHNAME;
+        (prompts)->challenge = "Authentication Name";
+        (prompts)->prompt = auth_prompt;
+        (prompts)->defresult = auth_def;
+
+        prompts++;
+    }
+
+    if (pass_prompt) {
+        (prompts)->id = SASL_CB_PASS;
+        (prompts)->challenge = "Password";
+        (prompts)->prompt = pass_prompt;
+        (prompts)->defresult = pass_def;
+
+        prompts++;
+    }
+
+    if (echo_prompt) {
+        (prompts)->id = SASL_CB_ECHOPROMPT;
+        (prompts)->challenge = echo_chal;
+        (prompts)->prompt = echo_prompt;
+        (prompts)->defresult = echo_def;
+
+        prompts++;
+    }
+
+    if (realm_prompt) {
+        (prompts)->id = SASL_CB_GETREALM;
+        (prompts)->challenge = realm_chal;
+        (prompts)->prompt = realm_prompt;
+        (prompts)->defresult = realm_def;
+
+        prompts++;
+    }
+
+    /* add the ending one */
+    (prompts)->id = SASL_CB_LIST_END;
+    (prompts)->challenge = NULL;
+    (prompts)->prompt = NULL;
+    (prompts)->defresult = NULL;
+
+    return SASL_OK;
+}
+
+void _plug_decode_init(decode_context_t *text,
+                       const sasl_utils_t *utils, unsigned int in_maxbuf)
+{
+    memset(text, 0, sizeof(decode_context_t));
+
+    text->utils = utils;
+    text->needsize = 4;
+    text->in_maxbuf = in_maxbuf;
+}
+
+/*
+ * Decode as much of the input as possible (possibly none),
+ * using decode_pkt() to decode individual packets.
+ */
+int _plug_decode(decode_context_t *text,
+                 const char *input, unsigned inputlen,
+                 char **output,     /* output buffer */
+                 unsigned *outputsize,  /* current size of output buffer */
+                 unsigned *outputlen,   /* length of data in output buffer */
+                 int (*decode_pkt)(void *rock,
+                                   const char *input, unsigned inputlen,
+                                   char **output, unsigned *outputlen),
+                 void *rock)
+{
+    unsigned int tocopy;
+    unsigned diff;
+    char *tmp;
+    unsigned tmplen;
+    int ret;
+
+    *outputlen = 0;
+
+    while (inputlen) { /* more input */
+        if (text->needsize) { /* need to get the rest of the 4-byte size */
+
+            /* copy as many bytes (up to 4) as we have into size buffer */
+            tocopy = (inputlen > text->needsize) ? text->needsize : inputlen;
+            memcpy(text->sizebuf + 4 - text->needsize, input, tocopy);
+            text->needsize -= tocopy;
+
+            input += tocopy;
+            inputlen -= tocopy;
+
+            if (!text->needsize) { /* we have the entire 4-byte size */
+                memcpy(&(text->size), text->sizebuf, 4);
+                text->size = ntohl(text->size);
+
+                if (!text->size) { /* should never happen */
+                    return SASL_FAIL;
+                }
+
+                if (text->size > text->in_maxbuf) {
+                    text->utils->log(NULL, SASL_LOG_ERR,
+                                     "encoded packet size too big (%d > %d)",
+                                     text->size, text->in_maxbuf);
+                    return SASL_FAIL;
+                }
+
+                if (!text->buffer) {
+                    text->buffer = text->utils->malloc(text->in_maxbuf);
+                }
+                if (text->buffer == NULL) {
+                    return SASL_NOMEM;
+                }
+
+                text->cursize = 0;
+            } else {
+                /* We do NOT have the entire 4-byte size...
+                 * wait for more data */
+                return SASL_OK;
+            }
+        }
+
+        diff = text->size - text->cursize; /* bytes needed for full packet */
+
+        if (inputlen < diff) {  /* not a complete packet, need more input */
+            memcpy(text->buffer + text->cursize, input, inputlen);
+            text->cursize += inputlen;
+            return SASL_OK;
+        }
+
+        /* copy the rest of the packet */
+        memcpy(text->buffer + text->cursize, input, diff);
+        input += diff;
+        inputlen -= diff;
+
+        /* decode the packet (no need to free tmp) */
+        ret = decode_pkt(rock, text->buffer, text->size, &tmp, &tmplen);
+        if (ret != SASL_OK) {
+            return ret;
+        }
+
+        /* append the decoded packet to the output */
+        ret = _plug_buf_alloc(text->utils, output, outputsize,
+                              *outputlen + tmplen + 1); /* +1 for NUL */
+        if (ret != SASL_OK) {
+            return ret;
+        }
+
+        memcpy(*output + *outputlen, tmp, tmplen);
+        *outputlen += tmplen;
+
+        /* protect stupid clients */
+        *(*output + *outputlen) = '\0';
+
+        /* reset for the next packet */
+        text->needsize = 4;
+    }
+
+    return SASL_OK;
+}
+
+void _plug_decode_free(decode_context_t *text)
+{
+    if (text->buffer) {
+        text->utils->free(text->buffer);
+    }
+}
+
+/* returns the realm we should pretend to be in */
+int _plug_parseuser(const sasl_utils_t *utils,
+                    char **user, char **realm, const char *user_realm,
+                    const char *serverFQDN, const char *input)
+{
+    int ret;
+    char *r;
+
+    if (!user || !serverFQDN) {
+        PARAMERROR(utils);
+        return SASL_BADPARAM;
+    }
+
+    r = strchr(input, '@');
+    if (!r) {
+        /* hmmm, the user didn't specify a realm */
+        if (user_realm && user_realm[0]) {
+            ret = _plug_strdup(utils, user_realm, realm, NULL);
+        } else {
+            /* Default to serverFQDN */
+            ret = _plug_strdup(utils, serverFQDN, realm, NULL);
+        }
+
+        if (ret == SASL_OK) {
+            ret = _plug_strdup(utils, input, user, NULL);
+        }
+    } else {
+        r++;
+        ret = _plug_strdup(utils, r, realm, NULL);
+        *--r = '\0';
+        *user = utils->malloc(r - input + 1);
+        if (*user) {
+            strncpy(*user, input, r - input + 1);
+        } else {
+            MEMERROR(utils);
+            ret = SASL_NOMEM;
+        }
+        *r = '@';
+    }
+
+    return ret;
+}
+
+int _plug_make_fulluser(const sasl_utils_t *utils,
+                        char **fulluser,
+                        const char *useronly,
+                        const char *realm)
+{
+    if (!fulluser || !useronly || !realm) {
+        PARAMERROR(utils);
+        return (SASL_BADPARAM);
+    }
+
+    *fulluser = utils->malloc(strlen(useronly) + strlen(realm) + 2);
+    if (*fulluser == NULL) {
+        MEMERROR(utils);
+        return (SASL_NOMEM);
+    }
+
+    strcpy(*fulluser, useronly);
+    strcat(*fulluser, "@");
+    strcat(*fulluser, realm);
+
+    return (SASL_OK);
+}
+
+char *_plug_get_error_message(const sasl_utils_t *utils,
+#ifdef WIN32
+                              DWORD error
+#else
+                              int error
+#endif
+                             )
+{
+    char *return_value;
+#ifdef WIN32
+    LPVOID lpMsgBuf;
+
+    FormatMessage(
+        FORMAT_MESSAGE_ALLOCATE_BUFFER |
+        FORMAT_MESSAGE_FROM_SYSTEM |
+        FORMAT_MESSAGE_IGNORE_INSERTS,
+        NULL,
+        error,
+        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), /* Default language */
+        (LPTSTR) &lpMsgBuf,
+        0,
+        NULL
+    );
+
+    if (_plug_strdup(utils, lpMsgBuf, &return_value, NULL) != SASL_OK) {
+        return_value = NULL;
+    }
+
+    LocalFree(lpMsgBuf);
+#else /* !WIN32 */
+    if (_plug_strdup(utils, strerror(error), &return_value, NULL) != SASL_OK) {
+        return_value = NULL;
+    }
+#endif /* WIN32 */
+    return (return_value);
+}
+
+void _plug_snprintf_os_info(char *osbuf, int osbuf_len)
+{
+#ifdef WIN32
+    OSVERSIONINFOEX versioninfo;
+    char *sysname;
+
+    /* :
+      DWORD dwOSVersionInfoSize;
+      DWORD dwMajorVersion;
+      DWORD dwMinorVersion;
+      DWORD dwBuildNumber;
+      TCHAR szCSDVersion[ 128 ];
+    //Only NT SP 6 and later
+      WORD wServicePackMajor;
+      WORD wServicePackMinor;
+      WORD wSuiteMask;
+      BYTE wProductType;
+     */
+
+    versioninfo.dwOSVersionInfoSize = sizeof(versioninfo);
+    sysname = "Unknown Windows";
+
+    if (GetVersionEx((OSVERSIONINFO *) &versioninfo) == FALSE) {
+        snprintf(osbuf, osbuf_len, "%s", sysname);
+        goto SKIP_OS_INFO;
+    }
+
+    switch (versioninfo.dwPlatformId) {
+    case VER_PLATFORM_WIN32s: /* Win32s on Windows 3.1 */
+        sysname = "Win32s on Windows 3.1";
+        /* I can't test if dwBuildNumber has any meaning on Win32s */
+        break;
+
+    case VER_PLATFORM_WIN32_WINDOWS: /* 95/98/ME */
+        switch (versioninfo.dwMinorVersion) {
+        case 0:
+            sysname = "Windows 95";
+            break;
+        case 10:
+            sysname = "Windows 98";
+            break;
+        case 90:
+            sysname = "Windows Me";
+            break;
+        default:
+            sysname = "Unknown Windows 9X/ME series";
+            break;
+        }
+        /* Clear the high order word, as it contains major/minor version */
+        versioninfo.dwBuildNumber &= 0xFFFF;
+        break;
+
+    case VER_PLATFORM_WIN32_NT: /* NT/2000/XP/.NET */
+        if (versioninfo.dwMinorVersion > 99) {
+        } else {
+            switch (versioninfo.dwMajorVersion * 100 + versioninfo.dwMinorVersion) {
+            case 351:
+                sysname = "Windows NT 3.51";
+                break;
+            case 400:
+                sysname = "Windows NT 4.0";
+                break;
+            case 500:
+                sysname = "Windows 2000";
+                break;
+            case 501:
+                sysname = "Windows XP/.NET"; /* or Windows .NET Server */
+                break;
+            default:
+                sysname = "Unknown Windows NT series";
+                break;
+            }
+        }
+        break;
+
+    default:
+        break;
+    }
+
+    snprintf(osbuf, osbuf_len,
+             "%s %s (Build %u)",
+             sysname,
+             versioninfo.szCSDVersion,
+             versioninfo.dwBuildNumber
+            );
+
+SKIP_OS_INFO:
+    ;
+
+#else /* !WIN32 */
+    struct utsname os;
+
+    uname(&os);
+    snprintf(osbuf, osbuf_len, "%s %s", os.sysname, os.release);
+#endif /* WIN32 */
+}
+
+#if defined(WIN32)
+unsigned int plug_sleep(unsigned int seconds)
+{
+    long dwSec = seconds * 1000;
+    Sleep(dwSec);
+    return 0;
+}
+#endif
diff --git a/resources/gmail/saslplugin/plugin_common.h b/resources/gmail/saslplugin/plugin_common.h
new file mode 100644 (file)
index 0000000..6cfbc5e
--- /dev/null
@@ -0,0 +1,221 @@
+
+/* Generic SASL plugin utility functions
+ * Rob Siemborski
+ * $Id: plugin_common.h,v 1.21 2006/01/17 12:18:21 mel Exp $
+ */
+/*
+ * Copyright (c) 1998-2003 Carnegie Mellon University.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the
+ *    distribution.
+ *
+ * 3. The name "Carnegie Mellon University" must not be used to
+ *    endorse or promote products derived from this software without
+ *    prior written permission. For permission or any other legal
+ *    details, please contact
+ *      Office of Technology Transfer
+ *      Carnegie Mellon University
+ *      5000 Forbes Avenue
+ *      Pittsburgh, PA  15213-3890
+ *      (412) 268-4387, fax: (412) 268-7395
+ *      tech-transfer@andrew.cmu.edu
+ *
+ * 4. Redistributions of any form whatsoever must retain the following
+ *    acknowledgment:
+ *    "This product includes software developed by Computing Services
+ *     at Carnegie Mellon University (http://www.cmu.edu/computing/)."
+ *
+ * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
+ * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
+ * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
+ * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef _PLUGIN_COMMON_H_
+#define _PLUGIN_COMMON_H_
+
+#include <config.h>
+
+#ifndef macintosh
+#ifdef WIN32
+# include <winsock2.h>
+#else
+# include <sys/socket.h>
+# include <netinet/in.h>
+# include <arpa/inet.h>
+# include <netdb.h>
+#endif /* WIN32 */
+#endif /* macintosh */
+
+#include <sasl.h>
+#include <saslutil.h>
+#include <saslplug.h>
+
+#ifdef WIN32
+#define PLUG_API __declspec(dllexport)
+#else
+#define PLUG_API extern
+#endif
+
+#define SASL_CLIENT_PLUG_INIT( x ) \
+    extern sasl_client_plug_init_t x##_client_plug_init; \
+    PLUG_API int sasl_client_plug_init(const sasl_utils_t *utils, \
+                                       int maxversion, int *out_version, \
+                                       sasl_client_plug_t **pluglist, \
+                                       int *plugcount) { \
+        return x##_client_plug_init(utils, maxversion, out_version, \
+                                    pluglist, plugcount); \
+    }
+
+#define SASL_SERVER_PLUG_INIT( x ) \
+    extern sasl_server_plug_init_t x##_server_plug_init; \
+    PLUG_API int sasl_server_plug_init(const sasl_utils_t *utils, \
+                                       int maxversion, int *out_version, \
+                                       sasl_server_plug_t **pluglist, \
+                                       int *plugcount) { \
+        return x##_server_plug_init(utils, maxversion, out_version, \
+                                    pluglist, plugcount); \
+    }
+
+#define SASL_AUXPROP_PLUG_INIT( x ) \
+    extern sasl_auxprop_init_t x##_auxprop_plug_init; \
+    PLUG_API int sasl_auxprop_plug_init(const sasl_utils_t *utils, \
+                                        int maxversion, int *out_version, \
+                                        sasl_auxprop_plug_t **plug, \
+                                        const char *plugname) {\
+        return x##_auxprop_plug_init(utils, maxversion, out_version, \
+                                     plug, plugname); \
+    }
+
+#define SASL_CANONUSER_PLUG_INIT( x ) \
+    extern sasl_canonuser_init_t x##_canonuser_plug_init; \
+    PLUG_API int sasl_canonuser_init(const sasl_utils_t *utils, \
+                                     int maxversion, int *out_version, \
+                                     sasl_canonuser_plug_t **plug, \
+                                     const char *plugname) {\
+        return x##_canonuser_plug_init(utils, maxversion, out_version, \
+                                       plug, plugname); \
+    }
+
+/* note: msg cannot include additional variables, so if you want to
+ * do a printf-format string, then you need to call seterror yourself */
+#define SETERROR( utils, msg ) (utils)->seterror( (utils)->conn, 0, (msg) )
+
+#ifndef MEMERROR
+#define MEMERROR( utils ) \
+    (utils)->seterror( (utils)->conn, 0, \
+                       "Out of Memory in " __FILE__ " near line %d", __LINE__ )
+#endif
+
+#ifndef PARAMERROR
+#define PARAMERROR( utils ) \
+    (utils)->seterror( (utils)->conn, 0, \
+                       "Parameter Error in " __FILE__ " near line %d", __LINE__ )
+#endif
+
+#ifndef SASLINT_H
+typedef struct buffer_info {
+    char *data;
+    unsigned curlen;   /* Current length of data in buffer */
+    unsigned reallen;  /* total length of buffer (>= curlen) */
+} buffer_info_t;
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+int _plug_ipfromstring(const sasl_utils_t *utils, const char *addr,
+                       struct sockaddr *out, socklen_t outlen);
+int _plug_iovec_to_buf(const sasl_utils_t *utils, const struct iovec *vec,
+                       unsigned numiov, buffer_info_t **output);
+int _plug_buf_alloc(const sasl_utils_t *utils, char **rwbuf,
+                    unsigned *curlen, unsigned newlen);
+int _plug_strdup(const sasl_utils_t *utils, const char *in,
+                 char **out, int *outlen);
+void _plug_free_string(const sasl_utils_t *utils, char **str);
+void _plug_free_secret(const sasl_utils_t *utils, sasl_secret_t **secret);
+
+#define _plug_get_userid(utils, result, prompt_need) \
+    _plug_get_simple(utils, SASL_CB_USER, 0, result, prompt_need)
+#define _plug_get_authid(utils, result, prompt_need) \
+    _plug_get_simple(utils, SASL_CB_AUTHNAME, 1, result, prompt_need)
+int _plug_get_simple(const sasl_utils_t *utils, unsigned int id, int required,
+                     const char **result, sasl_interact_t **prompt_need);
+
+int _plug_get_password(const sasl_utils_t *utils, sasl_secret_t **secret,
+                       unsigned int *iscopy, sasl_interact_t **prompt_need);
+
+int _plug_challenge_prompt(const sasl_utils_t *utils, unsigned int id,
+                           const char *challenge, const char *promptstr,
+                           const char **result, sasl_interact_t **prompt_need);
+
+int _plug_get_realm(const sasl_utils_t *utils, const char **availrealms,
+                    const char **realm, sasl_interact_t **prompt_need);
+
+int _plug_make_prompts(const sasl_utils_t *utils,
+                       sasl_interact_t **prompts_res,
+                       const char *user_prompt, const char *user_def,
+                       const char *auth_prompt, const char *auth_def,
+                       const char *pass_prompt, const char *pass_def,
+                       const char *echo_chal,
+                       const char *echo_prompt, const char *echo_def,
+                       const char *realm_chal,
+                       const char *realm_prompt, const char *realm_def);
+
+typedef struct decode_context {
+    const sasl_utils_t *utils;
+    unsigned int needsize;  /* How much of the 4-byte size do we need? */
+    char sizebuf[4];        /* Buffer to accumulate the 4-byte size */
+    unsigned int size;      /* Absolute size of the encoded packet */
+    char *buffer;       /* Buffer to accumulate an encoded packet */
+    unsigned int cursize;   /* Amount of packet data in the buffer */
+    unsigned int in_maxbuf; /* Maximum allowed size of an incoming encoded packet */
+} decode_context_t;
+
+void _plug_decode_init(decode_context_t *text,
+                       const sasl_utils_t *utils, unsigned int in_maxbuf);
+
+int _plug_decode(decode_context_t *text,
+                 const char *input, unsigned inputlen,
+                 char **output, unsigned *outputsize, unsigned *outputlen,
+                 int (*decode_pkt)(void *rock,
+                                   const char *input, unsigned inputlen,
+                                   char **output, unsigned *outputlen),
+                 void *rock);
+
+void _plug_decode_free(decode_context_t *text);
+
+int _plug_parseuser(const sasl_utils_t *utils,
+                    char **user, char **realm, const char *user_realm,
+                    const char *serverFQDN, const char *input);
+
+int _plug_make_fulluser(const sasl_utils_t *utils,
+                        char **fulluser, const char *useronly, const char *realm);
+
+char *_plug_get_error_message(const sasl_utils_t *utils,
+#ifdef WIN32
+                              DWORD error
+#else
+                              int error
+#endif
+                             );
+void _plug_snprintf_os_info(char *osbuf, int osbuf_len);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _PLUGIN_COMMON_H_ */
diff --git a/resources/gmail/saslplugin/xoauth2plugin.c b/resources/gmail/saslplugin/xoauth2plugin.c
new file mode 100644 (file)
index 0000000..ec66ca5
--- /dev/null
@@ -0,0 +1,241 @@
+/*
+    Copyright (c) 2014 Daniel Vrátil <dvratil@redhat.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+/**
+ * This is a very simple and single-purpose implementation of XOAUTH2 protocol
+ * for SASL used by KDE's KIMAP and Gmail resource in order to support OAuth
+ * Gmail authentication.
+ *
+ * This plugin does not even have a server-side part, and the client-side makes
+ * assumptions about it's use related to how KIMAP's LoginJob is implemented,
+ * so it's unsuitable for general-purpose use.
+ *
+ * The plugin is called libkdexoauth2.so to avoid conflict in case anyone ever
+ * writes a proper XOAUTH2 plugin for SASL.
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include <sasl/sasl.h>
+#include <sasl/saslplug.h>
+#include <sasl/saslutil.h>
+
+#include "plugin_common.h"
+
+/***************************** Common Section *****************************/
+
+static const char plugin_id[] = "$Id: plain.c,v 1.64 2004/09/08 11:06:11 mel Exp $";
+
+/***************************** Client Section *****************************/
+
+typedef struct client_context {
+    char *out_buf;
+    unsigned out_buf_len;
+} client_context_t;
+
+static int xoauth2_client_mech_new(void *glob_context,
+                                   sasl_client_params_t *params,
+                                   void **conn_context)
+{
+    client_context_t *context;
+
+    /* Q_UNUSED */
+    (void)glob_context;
+
+    /* holds state are in */
+    context = params->utils->malloc(sizeof(client_context_t));
+    if (context == NULL) {
+        MEMERROR(params->utils);
+        return SASL_NOMEM;
+    }
+
+    memset(context, 0, sizeof(client_context_t));
+
+    *conn_context = context;
+
+    return SASL_OK;
+}
+
+static int xoauth2_client_mech_step(void *conn_context,
+                                    sasl_client_params_t *params,
+                                    const char *serverin,
+                                    unsigned serverinlen,
+                                    sasl_interact_t **prompt_need,
+                                    const char **clientout,
+                                    unsigned *clientoutlen,
+                                    sasl_out_params_t *oparams)
+{
+    client_context_t *context = (client_context_t *) conn_context;
+    const sasl_utils_t *utils = params->utils;
+    const char *authid = NULL, *token = NULL;
+    int auth_result = SASL_OK;
+    int token_result = SASL_OK;
+    int result;
+    *clientout = NULL;
+    *clientoutlen = 0;
+
+    /* Q_UNUSED */
+    (void) serverin;
+    (void) serverinlen;
+
+    if (params->props.min_ssf > params->external_ssf) {
+        SETERROR(params->utils, "SSF requested of XOAUTH2 plugin");
+        return SASL_TOOWEAK;
+    }
+
+    /* try to get the authid */
+    if (oparams->authid == NULL) {
+        auth_result = _plug_get_authid(utils, &authid, prompt_need);
+        if ((auth_result != SASL_OK) && (auth_result != SASL_INTERACT)) {
+            return auth_result;
+        }
+    }
+
+    /* try to get oauth token */
+    if (token == NULL) {
+        /* We don't use _plug_get_password because we don't really care much about
+           safety of the OAuth token */
+        token_result = _plug_get_simple(utils, SASL_CB_PASS, 1, &token, prompt_need);
+        if ((token_result != SASL_OK) && (token_result != SASL_INTERACT)) {
+            return token_result;
+        }
+    }
+
+    /* free prompts we got */
+    if (prompt_need && *prompt_need) {
+        utils->free(*prompt_need);
+        *prompt_need = NULL;
+    }
+
+    /* if there are prompts not filled in */
+    if ((auth_result == SASL_INTERACT) || (token_result == SASL_INTERACT)) {
+        /* make the prompt list */
+        result =
+            _plug_make_prompts(utils, prompt_need,
+                               NULL, NULL,
+                               auth_result == SASL_INTERACT ?
+                               "Please enter your authentication name" : NULL,
+                               NULL,
+                               token_result == SASL_INTERACT ?
+                               "Please enter OAuth token" : NULL, NULL,
+                               NULL, NULL, NULL,
+                               NULL, NULL, NULL);
+        if (result != SASL_OK) {
+            return result;
+        }
+
+        return SASL_INTERACT;
+    }
+
+    /* FIXME: Fail if username is missing too? */
+    if (!token) {
+        PARAMERROR(params->utils);
+        return SASL_BADPARAM;
+    }
+
+    result = params->canon_user(utils->conn, authid, 0,
+                                SASL_CU_AUTHID | SASL_CU_AUTHZID, oparams);
+    if (result != SASL_OK) {
+        return result;
+    }
+
+    /* https://developers.google.com/gmail/xoauth2_protocol#the_sasl_xoauth2_mechanism */
+    *clientoutlen = 5                                               /* user=*/
+                    + ((authid && *authid) ? strlen(authid) : 0)      /* %s */
+                    + 1                                               /* \001 */
+                    + 12                                              /* auth=Bearer{space} */
+                    + ((token && *token) ? strlen(token) : 0)         /* %s */
+                    + 2;                                              /* \001\001 */
+
+    /* remember the extra NUL on the end for stupid clients */
+    result = _plug_buf_alloc(params->utils, &(context->out_buf),
+                             &(context->out_buf_len), *clientoutlen + 1);
+    if (result != SASL_OK) {
+        return result;
+    }
+
+    snprintf(context->out_buf, context->out_buf_len, "user=%s\1auth=Bearer %s\1\1", authid, token);
+
+    *clientout = context->out_buf;
+
+    /* set oparams */
+    oparams->doneflag = 1;
+    oparams->mech_ssf = 0;
+    oparams->maxoutbuf = 0;
+    oparams->encode_context = NULL;
+    oparams->encode = NULL;
+    oparams->decode_context = NULL;
+    oparams->decode = NULL;
+    oparams->param_version = 0;
+
+    return SASL_OK;
+}
+
+static void xoauth2_client_mech_dispose(void *conn_context,
+                                        const sasl_utils_t *utils)
+{
+    client_context_t *context = (client_context_t *) conn_context;
+    if (!context) {
+        return;
+    }
+
+    if (context->out_buf) {
+        printf("%s", context->out_buf);
+        utils->free(context->out_buf);
+    }
+    utils->free(context);
+}
+
+static sasl_client_plug_t xoauth2_client_plugins[] = {
+    {
+        "XOAUTH2",                      /* mech_name */
+        0,                              /* max_ssf */
+        SASL_SEC_NOANONYMOUS
+        | SASL_SEC_PASS_CREDENTIALS,    /* security_flags */
+        SASL_FEAT_WANT_CLIENT_FIRST
+        | SASL_FEAT_ALLOWS_PROXY,       /* features */
+        NULL,                           /* required_prompts */
+        NULL,                           /* glob_context */
+        &xoauth2_client_mech_new,       /* mech_new */
+        &xoauth2_client_mech_step,      /* mech_step */
+        &xoauth2_client_mech_dispose,   /* mech_dispose */
+        NULL,                           /* mech_free */
+        NULL,                           /* idle */
+        NULL,                           /* spare */
+        NULL                            /* spare */
+    }
+};
+
+int xoauth2_client_plug_init(sasl_utils_t *utils,
+                             int maxversion,
+                             int *out_version,
+                             sasl_client_plug_t **pluglist,
+                             int *plugcount)
+{
+    if (maxversion < SASL_CLIENT_PLUG_VERSION) {
+        SETERROR(utils, "XOAUTH2 version mismatch");
+        return SASL_BADVERS;
+    }
+
+    *out_version = SASL_CLIENT_PLUG_VERSION;
+    *pluglist = xoauth2_client_plugins;
+    *plugcount = 1;
+
+    return SASL_OK;
+}
diff --git a/resources/gmail/saslplugin/xoauth2plugin_init.c b/resources/gmail/saslplugin/xoauth2plugin_init.c
new file mode 100644 (file)
index 0000000..90028b2
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+    Copyright (c) 2014 Daniel Vrátil <dvratil@redhat.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "config.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#ifndef macintosh
+#include <sys/stat.h>
+#endif
+#include <fcntl.h>
+#include <assert.h>
+
+#include <sasl.h>
+#include <saslplug.h>
+#include <saslutil.h>
+
+#include "plugin_common.h"
+
+#ifdef macintosh
+#include <sasl_oauth_new_plugin_decl.h>
+#endif
+
+#ifdef WIN32
+BOOL APIENTRY DllMain(HANDLE hModule,
+                      DWORD ul_reason_for_call,
+                      LPVOID lpReserved)
+{
+    switch (ul_reason_for_call) {
+    case DLL_PROCESS_ATTACH:
+    case DLL_THREAD_ATTACH:
+    case DLL_THREAD_DETACH:
+    case DLL_PROCESS_DETACH:
+        break;
+    }
+    return TRUE;
+}
+#endif
+
+SASL_CLIENT_PLUG_INIT(xoauth2)
diff --git a/resources/gmail/settingsbase.kcfgc b/resources/gmail/settingsbase.kcfgc
new file mode 100644 (file)
index 0000000..2c1ec47
--- /dev/null
@@ -0,0 +1,6 @@
+File=gmailresource.kcfg
+ClassName=SettingsBase
+Mutators=true
+SetUserTexts=true
+Singleton=false
+GlobalEnums=true
diff --git a/resources/google/CMakeLists.txt b/resources/google/CMakeLists.txt
new file mode 100644 (file)
index 0000000..8dd343a
--- /dev/null
@@ -0,0 +1,15 @@
+include_directories(${CMAKE_CURRENT_SOURCE_DIR})
+
+add_definitions( -DQT_NO_CAST_FROM_ASCII )
+add_definitions( -DQT_NO_CAST_TO_ASCII )
+
+
+if(${AccountsQt_FOUND} AND ${SignOnQt_FOUND})
+  include_directories(${ACCOUNTSQT_INCLUDE_DIRS} ${SIGNONQT_INCLUDE_DIRS} ../)
+  add_definitions(-DHAVE_ACCOUNTS)
+  set(accounts_SRCS ../../shared/singlefileresource/getcredentialsjob.cpp)
+endif()
+
+add_subdirectory(calendar)
+add_subdirectory(contacts)
+
diff --git a/resources/google/calendar/CMakeLists.txt b/resources/google/calendar/CMakeLists.txt
new file mode 100644 (file)
index 0000000..cc6d44e
--- /dev/null
@@ -0,0 +1,70 @@
+
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_googlecalendar_resource\")
+
+
+set(calendarresource_SRCS
+  calendarresource.cpp
+  defaultreminderattribute.cpp
+  settings.cpp
+  settingsdialog.cpp
+  ../common/googleresource.cpp
+  ../common/googleaccountmanager.cpp
+  ../common/googlesettings.cpp
+  ../common/googlesettingsdialog.cpp
+  ${accounts_SRCS}
+)
+
+kconfig_add_kcfg_files(calendarresource_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/settingsbase.kcfgc)
+
+kcfg_generate_dbus_interface(
+  ${CMAKE_CURRENT_SOURCE_DIR}/settingsbase.kcfg
+  org.kde.Akonadi.GoogleCalendar.Settings
+)
+
+qt5_add_dbus_adaptor(calendarresource_SRCS
+  ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.GoogleCalendar.Settings.xml
+  ${CMAKE_CURRENT_SOURCE_DIR}/settings.h Settings
+)
+
+add_executable(akonadi_googlecalendar_resource ${calendarresource_SRCS})
+
+if( APPLE )
+  set_target_properties(akonadi_googlecalendar_resource PROPERTIES
+    MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../../Info.plist.template
+  )
+  set_target_properties(akonadi_googlecalendar_resource PROPERTIES
+    MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.googlecalendar"
+  )
+  set_target_properties(akonadi_googlecalendar_resource PROPERTIES
+    MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi Google Calendar Resource"
+  )
+endif()
+
+
+
+
+target_link_libraries(akonadi_googlecalendar_resource
+  KF5::AkonadiCore
+  KF5::CalendarCore
+  KF5::AkonadiCalendar
+  KF5::GAPICalendar
+  KF5::GAPICore
+  KF5::GAPITasks
+  KF5::AkonadiAgentBase
+  KF5::Wallet
+  KF5::I18n
+)
+
+if(${AccountsQt_FOUND} AND ${SignOnQt_FOUND})
+  target_link_libraries(akonadi_googlecalendar_resource
+    ${ACCOUNTSQT_LIBRARIES}
+    ${SIGNONQT_LIBRARIES})
+endif()
+
+
+install(TARGETS akonadi_googlecalendar_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
+
+install(
+  FILES googlecalendarresource.desktop
+  DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents"
+)
diff --git a/resources/google/calendar/Messages.sh b/resources/google/calendar/Messages.sh
new file mode 100644 (file)
index 0000000..bd80228
--- /dev/null
@@ -0,0 +1,4 @@
+#! /usr/bin/env bash
+$EXTRACTRC *.ui *.kcfg >> rc.cpp
+$XGETTEXT *.cpp -o $podir/akonadi_googlecalendar_resource.pot
+$XGETTEXT ../common/*.cpp -j -o $podir/akonadi_googlecalendar_resource.pot
diff --git a/resources/google/calendar/calendarresource.cpp b/resources/google/calendar/calendarresource.cpp
new file mode 100644 (file)
index 0000000..413b545
--- /dev/null
@@ -0,0 +1,824 @@
+/*
+    Copyright (C) 2011-2013  Daniel Vrátil <dvratil@redhat.com>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "calendarresource.h"
+#include "defaultreminderattribute.h"
+#include "settings.h"
+#include "settingsdialog.h"
+
+#include <AkonadiCore/Attribute>
+#include <AkonadiCore/AttributeFactory>
+#include <AkonadiCore/CollectionModifyJob>
+#include <AkonadiCore/EntityDisplayAttribute>
+#include <AkonadiCore/ItemFetchJob>
+#include <AkonadiCore/ItemFetchScope>
+#include <AkonadiCore/ItemModifyJob>
+#include <Akonadi/Calendar/BlockAlarmsAttribute>
+#include <AkonadiCore/CachePolicy>
+#include <AkonadiCore/VectorHelper>
+
+#include <KCalCore/Calendar>
+#include <KCalCore/Attendee>
+#include <KCalCore/ICalFormat>
+#include <KCalCore/FreeBusy>
+
+#include <KLocalizedString>
+#include <QDialog>
+#include <QIcon>
+
+#include <KGAPI/Calendar/Calendar>
+#include <KGAPI/Calendar/CalendarCreateJob>
+#include <KGAPI/Calendar/CalendarDeleteJob>
+#include <KGAPI/Calendar/CalendarFetchJob>
+#include <KGAPI/Calendar/CalendarModifyJob>
+#include <KGAPI/Calendar/Event>
+#include <KGAPI/Calendar/EventCreateJob>
+#include <KGAPI/Calendar/EventDeleteJob>
+#include <KGAPI/Calendar/EventFetchJob>
+#include <KGAPI/Calendar/EventModifyJob>
+#include <KGAPI/Calendar/EventMoveJob>
+#include <KGAPI/Calendar/FreeBusyQueryJob>
+#include <KGAPI/Tasks/Task>
+#include <KGAPI/Tasks/TaskCreateJob>
+#include <KGAPI/Tasks/TaskDeleteJob>
+#include <KGAPI/Tasks/TaskFetchJob>
+#include <KGAPI/Tasks/TaskModifyJob>
+#include <KGAPI/Tasks/TaskMoveJob>
+#include <KGAPI/Tasks/TaskList>
+#include <KGAPI/Tasks/TaskListCreateJob>
+#include <KGAPI/Tasks/TaskListDeleteJob>
+#include <KGAPI/Tasks/TaskListFetchJob>
+#include <KGAPI/Tasks/TaskListModifyJob>
+
+#include <KGAPI/Account>
+
+#define ROOT_COLLECTION_REMOTEID QStringLiteral("RootCollection")
+#define CALENDARS_PROPERTY "_KGAPI2CalendarPtr"
+#define TASK_PROPERTY "_KGAPI2::TaskPtr"
+
+Q_DECLARE_METATYPE(KGAPI2::ObjectsList)
+Q_DECLARE_METATYPE(KGAPI2::TaskPtr)
+
+using namespace Akonadi;
+using namespace KGAPI2;
+
+CalendarResource::CalendarResource(const QString &id):
+    GoogleResource(id)
+{
+    AttributeFactory::registerAttribute< DefaultReminderAttribute >();
+    updateResourceName();
+}
+
+CalendarResource::~CalendarResource()
+{
+}
+
+GoogleSettings *CalendarResource::settings() const
+{
+    return Settings::self();
+}
+
+int CalendarResource::runConfigurationDialog(WId windowId)
+{
+    QScopedPointer<SettingsDialog> settingsDialog(new SettingsDialog(accountManager(), windowId, this));
+    settingsDialog->setWindowIcon(QIcon::fromTheme(QStringLiteral("im-google")));
+
+    return settingsDialog->exec();
+}
+
+void CalendarResource::updateResourceName()
+{
+    const QString accountName = Settings::self()->account();
+    setName(i18nc("%1 is account name (user@gmail.com)", "Google Calendars and Tasks (%1)", accountName.isEmpty() ? i18n("not configured") : accountName));
+}
+
+QList< QUrl > CalendarResource::scopes() const
+{
+    QList<QUrl> scopes;
+    scopes << Account::calendarScopeUrl()
+           << Account::tasksScopeUrl();
+
+    return scopes;
+}
+
+void CalendarResource::retrieveItems(const Akonadi::Collection &collection)
+{
+    if (!canPerformTask()) {
+        return;
+    }
+
+    // https://bugs.kde.org/show_bug.cgi?id=308122: we can only request changes in
+    // max. last 25 days, otherwise we get an error.
+    int lastSyncDelta = -1;
+    if (!collection.remoteRevision().isEmpty()) {
+        lastSyncDelta = QDateTime::currentDateTimeUtc().toTime_t() - collection.remoteRevision().toUInt();
+    }
+
+    KGAPI2::Job *job = Q_NULLPTR;
+    if (collection.contentMimeTypes().contains(KCalCore::Event::eventMimeType())) {
+        EventFetchJob *fetchJob = new EventFetchJob(collection.remoteId(), account(), this);
+        if (lastSyncDelta > -1 && lastSyncDelta < 25 * 24 * 3600) {
+            fetchJob->setFetchOnlyUpdated(collection.remoteRevision().toULongLong());
+        }
+        if (!Settings::self()->eventsSince().isEmpty()) {
+            const QDate date = QDate::fromString(Settings::self()->eventsSince(), Qt::ISODate);
+            fetchJob->setTimeMin(QDateTime(date).toTime_t());
+        }
+        job = fetchJob;
+    } else if (collection.contentMimeTypes().contains(KCalCore::Todo::todoMimeType())) {
+        TaskFetchJob *fetchJob = new TaskFetchJob(collection.remoteId(), account(), this);
+        if (lastSyncDelta > -1 && lastSyncDelta < 25 * 25 * 3600) {
+            fetchJob->setFetchOnlyUpdated(collection.remoteRevision().toULongLong());
+        }
+        job = fetchJob;
+    } else {
+        itemsRetrieved(Item::List());
+        return;
+    }
+
+    job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection));
+    connect(job, &KGAPI2::Job::progress, this, &CalendarResource::emitPercent);
+    connect(job, &KGAPI2::Job::finished, this, &CalendarResource::slotItemsRetrieved);
+}
+
+void CalendarResource::retrieveCollections()
+{
+    if (!canPerformTask()) {
+        return;
+    }
+
+    CalendarFetchJob *fetchJob = new CalendarFetchJob(account(), this);
+    connect(fetchJob, &EventFetchJob::finished, this, &CalendarResource::slotCalendarsRetrieved);
+}
+
+void CalendarResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection)
+{
+    if ((!collection.contentMimeTypes().contains(KCalCore::Event::eventMimeType()) &&
+            !collection.contentMimeTypes().contains(KCalCore::Todo::todoMimeType())) ||
+            (!canPerformTask<KCalCore::Event::Ptr>(item, KCalCore::Event::eventMimeType()) &&
+             !canPerformTask<KCalCore::Todo::Ptr>(item, KCalCore::Todo::todoMimeType()))) {
+        return;
+    }
+
+    if (collection.parentCollection() == Akonadi::Collection::root()) {
+        cancelTask(i18n("The top-level collection cannot contain any tasks or events"));
+        return;
+    }
+
+    KGAPI2::Job *job = Q_NULLPTR;
+    if (item.hasPayload<KCalCore::Event::Ptr>()) {
+        KCalCore::Event::Ptr event = item.payload<KCalCore::Event::Ptr>();
+        EventPtr kevent(new Event(*event));
+        kevent->setUid(QLatin1String(""));
+
+        job = new EventCreateJob(kevent, collection.remoteId(), account(),  this);
+
+    } else if (item.hasPayload<KCalCore::Todo::Ptr>()) {
+        KCalCore::Todo::Ptr todo = item.payload<KCalCore::Todo::Ptr>();
+        TaskPtr ktodo(new Task(*todo));
+        ktodo->setUid(QLatin1String(""));
+
+        if (!ktodo->relatedTo(KCalCore::Incidence::RelTypeParent).isEmpty()) {
+            Akonadi::Item parentItem;
+            parentItem.setGid(ktodo->relatedTo(KCalCore::Incidence::RelTypeParent));
+
+            ItemFetchJob *fetchJob = new ItemFetchJob(parentItem, this);
+            fetchJob->setProperty(ITEM_PROPERTY, QVariant::fromValue(item));
+            fetchJob->setProperty(TASK_PROPERTY, QVariant::fromValue(ktodo));
+
+            connect(fetchJob, &ItemFetchJob::finished, this, &CalendarResource::slotTaskAddedSearchFinished);
+            return;
+        } else {
+            job = new TaskCreateJob(ktodo, collection.remoteId(), account(), this);
+        }
+    } else {
+        cancelTask(i18n("Invalid payload type"));
+        return;
+    }
+
+    job->setProperty(ITEM_PROPERTY, QVariant::fromValue(item));
+    connect(job, &EventCreateJob::finished, this, &CalendarResource::slotCreateJobFinished);
+}
+
+void CalendarResource::itemChanged(const Akonadi::Item &item,
+                                   const QSet< QByteArray > &partIdentifiers)
+{
+    Q_UNUSED(partIdentifiers);
+
+    if (!canPerformTask<KCalCore::Event::Ptr>(item, KCalCore::Event::eventMimeType()) &&
+            !canPerformTask<KCalCore::Todo::Ptr>(item, KCalCore::Todo::todoMimeType())) {
+        return;
+    }
+
+    KGAPI2::Job *job = Q_NULLPTR;
+    if (item.hasPayload<KCalCore::Event::Ptr>()) {
+        KCalCore::Event::Ptr event = item.payload<KCalCore::Event::Ptr>();
+        EventPtr kevent(new Event(*event));
+        kevent->setUid(item.remoteId());
+
+        job = new EventModifyJob(kevent, item.parentCollection().remoteId(), account(), this);
+        connect(job, &EventCreateJob::finished, this, &CalendarResource::slotGenericJobFinished);
+
+    } else if (item.hasPayload<KCalCore::Todo::Ptr>()) {
+        KCalCore::Todo::Ptr todo = item.payload<KCalCore::Todo::Ptr>();
+        TaskPtr ktodo(new Task(*todo));
+        QString parentUid = todo->relatedTo(KCalCore::Incidence::RelTypeParent);
+        job = new TaskMoveJob(item.remoteId(), item.parentCollection().remoteId(), parentUid, account(), this);
+        job->setProperty(ITEM_PROPERTY, QVariant::fromValue(item));
+        connect(job, &EventCreateJob::finished, this, &CalendarResource::slotModifyTaskReparentFinished);
+    } else {
+        cancelTask(i18n("Invalid payload type"));
+        return;
+    }
+
+    job->setProperty(ITEM_PROPERTY, QVariant::fromValue(item));
+}
+
+void CalendarResource::itemRemoved(const Akonadi::Item &item)
+{
+    if (!canPerformTask()) {
+        return;
+    }
+
+    if (item.mimeType() == KCalCore::Event::eventMimeType()) {
+        KGAPI2::Job *job = new EventDeleteJob(item.remoteId(), item.parentCollection().remoteId(), account(), this);
+        job->setProperty(ITEM_PROPERTY, QVariant::fromValue(item));
+        connect(job, &EventCreateJob::finished, this, &CalendarResource::slotGenericJobFinished);
+
+    } else if (item.mimeType() == KCalCore::Todo::todoMimeType()) {
+        /* Google always automatically removes tasks with all their subtasks. In KOrganizer
+         * by default we only remove the item we are given. For this reason we have to first
+         * fetch all tasks, find all sub-tasks for the task being removed and detach them
+         * from the task. Only then the task can be safely removed. */
+        ItemFetchJob *fetchJob = new ItemFetchJob(item.parentCollection());
+        fetchJob->setAutoDelete(true);
+        fetchJob->fetchScope().fetchFullPayload(true);
+        fetchJob->setProperty(ITEM_PROPERTY, qVariantFromValue(item));
+        connect(fetchJob, &ItemFetchJob::finished, this, &CalendarResource::slotRemoveTaskFetchJobFinished);
+        fetchJob->start();
+
+    } else {
+        cancelTask(i18n("Invalid payload type. Expected event or todo, got %1", item.mimeType()));
+    }
+}
+
+void CalendarResource::itemMoved(const Item &item,
+                                 const Collection &collectionSource,
+                                 const Collection &collectionDestination)
+{
+    if (!canPerformTask()) {
+        return;
+    }
+
+    if (collectionDestination.parentCollection() == Akonadi::Collection::root()) {
+        cancelTask(i18n("The top-level collection cannot contain any tasks or events"));
+        return;
+    }
+
+    if (item.mimeType() != KCalCore::Event::eventMimeType()) {
+        cancelTask(i18n("Moving tasks between task lists is not supported"));
+        return;
+    }
+
+    KGAPI2::Job *job = new EventMoveJob(item.remoteId(), collectionSource.remoteId(),
+                                        collectionDestination.remoteId(), account(),
+                                        this);
+    job->setProperty(ITEM_PROPERTY, QVariant::fromValue(item));
+    connect(job, &EventCreateJob::finished, this, &CalendarResource::slotGenericJobFinished);
+}
+
+void CalendarResource::collectionAdded(const Collection &collection, const Collection &parent)
+{
+    Q_UNUSED(parent)
+
+    if (!canPerformTask()) {
+        return;
+    }
+
+    KGAPI2::Job *job;
+    if (collection.contentMimeTypes().contains(KCalCore::Event::eventMimeType())) {
+        CalendarPtr calendar(new Calendar());
+        calendar->setTitle(collection.displayName());
+        calendar->setEditable(true);
+        job = new CalendarCreateJob(calendar, account(), this);
+
+    } if (collection.contentMimeTypes().contains(KCalCore::Todo::todoMimeType())) {
+        TaskListPtr taskList(new TaskList());
+        taskList->setTitle(collection.displayName());
+
+        job = new TaskListCreateJob(taskList, account(), this);
+    } else {
+        cancelTask(i18n("Unknown collection mimetype"));
+        return;
+    }
+
+    job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection));
+    connect(job, &KGAPI2::Job::finished, this, &CalendarResource::slotGenericJobFinished);
+}
+
+void CalendarResource::collectionChanged(const Collection &collection)
+{
+    if (!canPerformTask()) {
+        return;
+    }
+
+    KGAPI2::Job *job;
+    if (collection.contentMimeTypes().contains(KCalCore::Event::eventMimeType())) {
+        CalendarPtr calendar(new Calendar());
+        calendar->setUid(collection.remoteId());
+        calendar->setTitle(collection.displayName());
+        calendar->setEditable(true);
+        job = new CalendarModifyJob(calendar, account(), this);
+
+    } if (collection.contentMimeTypes().contains(KCalCore::Todo::todoMimeType())) {
+        TaskListPtr taskList(new TaskList());
+        taskList->setUid(collection.remoteId());
+        taskList->setTitle(collection.displayName());
+        job = new TaskListModifyJob(taskList, account(), this);
+    } else {
+        cancelTask(i18n("Unknown collection mimetype"));
+        return;
+    }
+
+    job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection));
+    connect(job, &KGAPI2::Job::finished, this, &CalendarResource::slotGenericJobFinished);
+}
+
+void CalendarResource::collectionRemoved(const Collection &collection)
+{
+    if (!canPerformTask()) {
+        return;
+    }
+
+    KGAPI2::Job *job;
+    if (collection.contentMimeTypes().contains(KCalCore::Event::eventMimeType())) {
+        job = new CalendarDeleteJob(collection.remoteId(), account(), this);
+
+    } if (collection.contentMimeTypes().contains(KCalCore::Todo::todoMimeType())) {
+        job = new TaskListDeleteJob(collection.remoteId(), account(), this);
+
+    } else {
+        cancelTask(i18n("Unknown collection mimetype"));
+        return;
+    }
+
+    job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection));
+    connect(job, &KGAPI2::Job::finished, this, &CalendarResource::slotGenericJobFinished);
+}
+
+void CalendarResource::slotCalendarsRetrieved(KGAPI2::Job *job)
+{
+    if (!handleError(job)) {
+        return;
+    }
+
+    CalendarFetchJob *calendarJob = qobject_cast<CalendarFetchJob *>(job);
+    ObjectsList objects = calendarJob->items();
+    calendarJob->deleteLater();
+
+    TaskListFetchJob *fetchJob = new TaskListFetchJob(job->account(), this);
+    fetchJob->setProperty(CALENDARS_PROPERTY, QVariant::fromValue(objects));
+    connect(fetchJob, &EventFetchJob::finished, this, &CalendarResource::slotCollectionsRetrieved);
+}
+
+void CalendarResource::slotCollectionsRetrieved(KGAPI2::Job *job)
+{
+    if (!handleError(job)) {
+        return;
+    }
+
+    TaskListFetchJob *fetchJob = qobject_cast<TaskListFetchJob *>(job);
+    ObjectsList calendars = fetchJob->property(CALENDARS_PROPERTY).value<ObjectsList>();
+    ObjectsList taskLists = fetchJob->items();
+
+    CachePolicy cachePolicy;
+    if (Settings::self()->enableIntervalCheck()) {
+        cachePolicy.setInheritFromParent(false);
+        cachePolicy.setIntervalCheckTime(Settings::self()->intervalCheckTime());
+    }
+
+    m_rootCollection = Collection();
+    m_rootCollection.setContentMimeTypes(QStringList() << Collection::mimeType());
+    m_rootCollection.setRemoteId(ROOT_COLLECTION_REMOTEID);
+    m_rootCollection.setName(fetchJob->account()->accountName());
+    m_rootCollection.setParentCollection(Collection::root());
+    m_rootCollection.setRights(Collection::CanCreateCollection);
+    m_rootCollection.setCachePolicy(cachePolicy);
+
+    EntityDisplayAttribute *attr = m_rootCollection.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
+    attr->setDisplayName(fetchJob->account()->accountName());
+    attr->setIconName(QStringLiteral("im-google"));
+
+    m_collections[ ROOT_COLLECTION_REMOTEID ] = m_rootCollection;
+
+    const QStringList activeCalendars = Settings::self()->calendars();
+    Q_FOREACH (const ObjectPtr &object, calendars) {
+        const CalendarPtr &calendar = object.dynamicCast<Calendar>();
+
+        if (!activeCalendars.contains(calendar->uid())) {
+            continue;
+        }
+
+        Collection collection;
+        collection.setContentMimeTypes(QStringList() << KCalCore::Event::eventMimeType());
+        collection.setName(calendar->uid());
+        collection.setParentCollection(m_rootCollection);
+        collection.setRemoteId(calendar->uid());
+        if (calendar->editable()) {
+            collection.setRights(Collection::CanChangeCollection |
+                                 Collection::CanCreateItem |
+                                 Collection::CanChangeItem |
+                                 Collection::CanDeleteItem);
+        } else {
+            collection.setRights(Q_NULLPTR);
+        }
+
+        EntityDisplayAttribute *attr = collection.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
+        attr->setDisplayName(calendar->title());
+        attr->setIconName(QStringLiteral("view-calendar"));
+
+        DefaultReminderAttribute *reminderAttr = collection.attribute<DefaultReminderAttribute>(Collection::AddIfMissing);
+        reminderAttr->setReminders(calendar->defaultReminders());
+
+        // Block email reminders, since Google sends them for us
+        BlockAlarmsAttribute *blockAlarms = collection.attribute<BlockAlarmsAttribute>(Collection::AddIfMissing);
+        blockAlarms->blockAlarmType(KCalCore::Alarm::Audio, false);
+        blockAlarms->blockAlarmType(KCalCore::Alarm::Display, false);
+        blockAlarms->blockAlarmType(KCalCore::Alarm::Procedure, false);
+
+        m_collections[ collection.remoteId() ] = collection;
+    }
+
+    const QStringList activeTaskLists = Settings::self()->taskLists();
+    Q_FOREACH (const ObjectPtr &object, taskLists) {
+        const TaskListPtr &taskList = object.dynamicCast<TaskList>();
+
+        if (!activeTaskLists.contains(taskList->uid())) {
+            continue;
+        }
+
+        Collection collection;
+        collection.setContentMimeTypes(QStringList() << KCalCore::Todo::todoMimeType());
+        collection.setName(taskList->uid());
+        collection.setParentCollection(m_rootCollection);
+        collection.setRemoteId(taskList->uid());
+        collection.setRights(Collection::CanChangeCollection |
+                             Collection::CanCreateItem |
+                             Collection::CanChangeItem |
+                             Collection::CanDeleteItem);
+
+        EntityDisplayAttribute *attr = collection.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
+        attr->setDisplayName(taskList->title());
+        attr->setIconName(QStringLiteral("view-pim-tasks"));
+
+        m_collections[ collection.remoteId() ] = collection;
+    }
+
+    collectionsRetrieved(Akonadi::valuesToVector(m_collections));
+
+    job->deleteLater();
+}
+
+void CalendarResource::slotItemsRetrieved(KGAPI2::Job *job)
+{
+    if (!handleError(job)) {
+        return;
+    }
+
+    Item::List changedItems, removedItems;
+    Collection collection = job->property(COLLECTION_PROPERTY).value<Collection>();
+    DefaultReminderAttribute *attr = collection.attribute<DefaultReminderAttribute>();
+    bool isIncremental = false;
+
+    ObjectsList objects = qobject_cast<FetchJob *>(job)->items();
+    if (collection.contentMimeTypes().contains(KCalCore::Event::eventMimeType())) {
+        QMap< QString, EventPtr > recurrentEvents;
+
+        isIncremental = (qobject_cast<EventFetchJob *>(job)->fetchOnlyUpdated() > 0);
+
+        /* Step 1: Find all recurrent events and move them to a separate map */
+        int i = 0;
+        while (i < objects.length()) {
+            EventPtr event = objects.at(i).dynamicCast<Event>();
+            if (event->recurs() && !event->deleted()) {
+                recurrentEvents.insert(event->uid(), event);
+                objects.removeAt(i);
+            } else {
+                i++;
+            }
+        }
+
+        /* Step 2: Process all remaining events */
+        Q_FOREACH (const ObjectPtr &object, objects) {
+            EventPtr event = object.dynamicCast<Event>();
+
+            /* If current event is related to a recurrent event stored in the map then
+             * take the original recurrent event, set date of the current event as an
+             * exception and continue. We will process content of the map later. */
+            if (recurrentEvents.contains(event->uid())) {
+                EventPtr rEvent = recurrentEvents.value(event->uid());
+                rEvent->recurrence()->addExDate(event->dtStart().date());
+                continue;
+            }
+
+            if (event->useDefaultReminders() && attr) {
+                KCalCore::Alarm::List alarms = attr->alarms(event.data());
+                Q_FOREACH (const KCalCore::Alarm::Ptr &alarm, alarms) {
+                    event->addAlarm(alarm);
+                }
+            }
+
+            Item item;
+            item.setMimeType(KCalCore::Event::eventMimeType());
+            item.setParentCollection(collection);
+            item.setRemoteId(event->uid());
+            item.setRemoteRevision(event->etag());
+            item.setPayload<KCalCore::Event::Ptr>(event.dynamicCast<KCalCore::Event>());
+
+            if (event->deleted()) {
+                removedItems << item;
+            } else {
+                changedItems << item;
+            }
+        }
+
+        /* Step 3: Now process the recurrent events */
+        Q_FOREACH (const EventPtr &event, recurrentEvents) {
+
+            Item item;
+            item.setRemoteId(event->uid());
+            item.setRemoteRevision(event->etag());
+            item.setPayload< KCalCore::Event::Ptr >(event.dynamicCast<KCalCore::Event>());
+            item.setMimeType(KCalCore::Event::eventMimeType());
+            item.setParentCollection(collection);
+
+            changedItems << item;
+        }
+
+    } else if (collection.contentMimeTypes().contains(KCalCore::Todo::todoMimeType())) {
+
+        isIncremental = (qobject_cast<TaskFetchJob *>(job)->fetchOnlyUpdated() > 0);
+
+        Q_FOREACH (const ObjectPtr &object, objects) {
+            TaskPtr task = object.dynamicCast<Task>();
+
+            Item item;
+            item.setMimeType(KCalCore::Todo::todoMimeType());
+            item.setParentCollection(collection);
+            item.setRemoteId(task->uid());
+            item.setRemoteRevision(task->etag());
+            item.setPayload<KCalCore::Todo::Ptr>(task.dynamicCast<KCalCore::Todo>());
+
+            if (task->deleted()) {
+                removedItems << item;
+            } else {
+                changedItems << item;
+            }
+        }
+    }
+
+    if (isIncremental) {
+        itemsRetrievedIncremental(changedItems, removedItems);
+    } else {
+        itemsRetrieved(changedItems);
+    }
+    const QDateTime local(QDateTime::currentDateTime());
+    const QDateTime UTC(local.toUTC());
+
+    collection.setRemoteRevision(QString::number(UTC.toTime_t()));
+    new CollectionModifyJob(collection, this);
+
+    job->deleteLater();
+}
+
+void CalendarResource::slotModifyTaskReparentFinished(KGAPI2::Job *job)
+{
+    if (!handleError(job)) {
+        return;
+    }
+
+    Item item = job->property(ITEM_PROPERTY).value<Item>();
+    KCalCore::Todo::Ptr todo = item.payload<KCalCore::Todo::Ptr>();
+    TaskPtr ktodo(new Task(*todo.data()));
+
+    job = new TaskModifyJob(ktodo, item.parentCollection().remoteId(), job->account(), this);
+    job->setProperty(ITEM_PROPERTY, QVariant::fromValue(item));
+    connect(job, &KGAPI2::Job::finished, this, &CalendarResource::slotGenericJobFinished);
+}
+
+void CalendarResource::slotRemoveTaskFetchJobFinished(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(i18n("Failed to delete task: %1", job->errorString()));
+        return;
+    }
+
+    ItemFetchJob *fetchJob = qobject_cast<ItemFetchJob *>(job);
+    Item removedItem = fetchJob->property(ITEM_PROPERTY).value<Item>();
+
+    Item::List detachItems;
+
+    Item::List items = fetchJob->items();
+    Q_FOREACH (Item item, items) {   //krazy:exclude=foreach
+        if (!item.hasPayload<KCalCore::Todo::Ptr>()) {
+            qDebug() << "Item " << item.remoteId() << " does not have Todo payload";
+            continue;
+        }
+
+        KCalCore::Todo::Ptr todo = item.payload<KCalCore::Todo::Ptr>();
+        /* If this item is child of the item we want to remove then add it to detach list */
+        if (todo->relatedTo(KCalCore::Incidence::RelTypeParent) == removedItem.remoteId()) {
+            todo->setRelatedTo(QString(), KCalCore::Incidence::RelTypeParent);
+            item.setPayload(todo);
+            detachItems << item;
+        }
+    }
+
+    /* If there are no items do detach, then delete the task right now */
+    if (detachItems.isEmpty()) {
+        slotDoRemoveTask(job);
+        return;
+    }
+
+    /* Send modify request to detach all the sub-tasks from the task that is about to be
+     * removed. */
+    ItemModifyJob *modifyJob = new ItemModifyJob(detachItems);
+    modifyJob->setProperty(ITEM_PROPERTY, QVariant::fromValue(removedItem));
+    modifyJob->setAutoDelete(true);
+    connect(modifyJob, &ItemModifyJob::finished, this, &CalendarResource::slotDoRemoveTask);
+}
+
+void CalendarResource::slotDoRemoveTask(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(i18n("Failed to delete task: %1", job->errorString()));
+        return;
+    }
+
+    // Make sure account is still valid
+    if (!canPerformTask()) {
+        return;
+    }
+
+    Item item = job->property(ITEM_PROPERTY).value< Item >();
+
+    /* Now finally we can safely remove the task we wanted to */
+    TaskDeleteJob *deleteJob = new TaskDeleteJob(item.remoteId(), item.parentCollection().remoteId(), account(), this);
+    deleteJob->setProperty(ITEM_PROPERTY, QVariant::fromValue(item));
+    connect(deleteJob, &TaskDeleteJob::finished, this, &CalendarResource::slotGenericJobFinished);
+}
+
+void CalendarResource::slotTaskAddedSearchFinished(KJob *job)
+{
+    ItemFetchJob *fetchJob = qobject_cast<ItemFetchJob *>(job);
+    Item item = job->property(ITEM_PROPERTY).value<Item>();
+    TaskPtr task = job->property(TASK_PROPERTY).value<TaskPtr>();
+
+    Item::List items = fetchJob->items();
+    qDebug() << "Parent query returned" << items.count() << "results";
+
+    const QString tasksListId = item.parentCollection().remoteId();
+
+    // Make sure account is still valid
+    if (!canPerformTask()) {
+        return;
+    }
+
+    KGAPI2::Job *newJob;
+    // The parent is not known, so give up and just store the item in Google
+    // without the information about parent.
+    if (items.count() == 0) {
+        task->setRelatedTo(QString(), KCalCore::Incidence::RelTypeParent);
+        newJob = new TaskCreateJob(task, tasksListId, account(), this);
+    } else {
+        Item matchedItem = items.first();
+
+        task->setRelatedTo(matchedItem.remoteId(), KCalCore::Incidence::RelTypeParent);
+        TaskCreateJob *createJob = new TaskCreateJob(task, tasksListId, account(), this);
+        createJob->setParentItem(matchedItem.remoteId());
+        newJob = createJob;
+    }
+
+    newJob->setProperty(ITEM_PROPERTY, QVariant::fromValue(item));
+    connect(newJob, &KGAPI2::Job::finished, this, &CalendarResource::slotCreateJobFinished);
+}
+
+void CalendarResource::slotCreateJobFinished(KGAPI2::Job *job)
+{
+    if (!handleError(job)) {
+        return;
+    }
+
+    Item item = job->property(ITEM_PROPERTY).value<Item>();
+
+    CreateJob *createJob = qobject_cast<CreateJob *>(job);
+    ObjectsList objects = createJob->items();
+    Q_ASSERT(objects.count() > 0);
+
+    if (item.mimeType() == KCalCore::Event::eventMimeType()) {
+        EventPtr event = objects.first().dynamicCast<Event>();
+        item.setRemoteId(event->uid());
+        item.setRemoteRevision(event->etag());
+        item.setGid(event->uid());
+        changeCommitted(item);
+        item.setPayload<KCalCore::Event::Ptr>(event.dynamicCast<KCalCore::Event>());
+        new ItemModifyJob(item, this);
+    } else if (item.mimeType() == KCalCore::Todo::todoMimeType()) {
+        TaskPtr task = objects.first().dynamicCast<Task>();
+        item.setRemoteId(task->uid());
+        item.setRemoteRevision(task->etag());
+        item.setGid(task->uid());
+        changeCommitted(item);
+        item.setPayload<KCalCore::Todo::Ptr>(task.dynamicCast<KCalCore::Todo>());
+        new ItemModifyJob(item, this);
+    }
+}
+
+KDateTime CalendarResource::lastCacheUpdate() const
+{
+    return KDateTime();
+}
+
+void CalendarResource::canHandleFreeBusy(const QString &email) const
+{
+    if (!const_cast<CalendarResource *>(this)->canPerformTask()) {
+        handlesFreeBusy(email, false);
+        return;
+    }
+
+    auto job = new KGAPI2::FreeBusyQueryJob(email,
+                                            QDateTime::currentDateTimeUtc(),
+                                            QDateTime::currentDateTimeUtc().addSecs(3600),
+                                            const_cast<CalendarResource *>(this)->account(),
+                                            const_cast<CalendarResource *>(this));
+    connect(job, &KGAPI2::Job::finished,
+            this, &CalendarResource::slotCanHandleFreeBusyJobFinished);
+}
+
+void CalendarResource::slotCanHandleFreeBusyJobFinished(KGAPI2::Job *job)
+{
+    auto queryJob = qobject_cast<KGAPI2::FreeBusyQueryJob *>(job);
+
+    if (!handleError(job, false)) {
+        handlesFreeBusy(queryJob->id(), false);
+        return;
+    }
+
+    handlesFreeBusy(queryJob->id(), true);
+}
+
+void CalendarResource::retrieveFreeBusy(const QString &email, const KDateTime &start,
+                                        const KDateTime &end)
+{
+    if (!const_cast<CalendarResource *>(this)->canPerformTask()) {
+        freeBusyRetrieved(email, QString(), false, QString());
+        return;
+    }
+
+    auto job = new KGAPI2::FreeBusyQueryJob(email, start.dateTime(), end.dateTime(),
+                                            account(), this);
+    connect(job, &KGAPI2::Job::finished,
+            this, &CalendarResource::slotRetrieveFreeBusyJobFinished);
+}
+
+void CalendarResource::slotRetrieveFreeBusyJobFinished(KGAPI2::Job *job)
+{
+    auto queryJob = qobject_cast<KGAPI2::FreeBusyQueryJob *>(job);
+
+    if (!handleError(job, false)) {
+        freeBusyRetrieved(queryJob->id(), QString(), false, QString());
+        return;
+    }
+
+    KCalCore::FreeBusy::Ptr fb(new KCalCore::FreeBusy);
+    fb->setUid(QStringLiteral("%1%2@google.com").arg(QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyyMMddTHHmmssZ"))));
+    fb->setOrganizer(account()->accountName());
+    fb->addAttendee(KCalCore::Attendee::Ptr(new KCalCore::Attendee(QString(), queryJob->id())));
+    // FIXME: is it really sort?
+    fb->setDateTime(KDateTime::currentUtcDateTime(), KCalCore::IncidenceBase::RoleSort);
+
+    Q_FOREACH (const KGAPI2::FreeBusyQueryJob::BusyRange &range, queryJob->busy()) {
+        fb->addPeriod(KDateTime(range.busyStart), KDateTime(range.busyEnd));
+    }
+
+    KCalCore::ICalFormat format;
+    const QString fbStr = format.createScheduleMessage(fb, KCalCore::iTIPRequest);
+
+    freeBusyRetrieved(queryJob->id(), fbStr, true, QString());
+}
+
+AKONADI_RESOURCE_MAIN(CalendarResource)
diff --git a/resources/google/calendar/calendarresource.h b/resources/google/calendar/calendarresource.h
new file mode 100644 (file)
index 0000000..a80fb3f
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+    Copyright (C) 2011-2013  Daniel Vrátil <dvratil@redhat.com>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef GOOGLE_CALENDAR_CALENDARRESOURCE_H
+#define GOOGLE_CALENDAR_CALENDARRESOURCE_H
+
+#include "common/googleresource.h"
+
+#include <AkonadiCore/Item>
+#include <AkonadiCore/Collection>
+#include <Akonadi/Calendar/FreeBusyProviderBase>
+
+class CalendarResource : public GoogleResource
+    , public Akonadi::FreeBusyProviderBase
+{
+    Q_OBJECT
+public:
+    explicit CalendarResource(const QString &id);
+    ~CalendarResource();
+
+public:
+    using GoogleResource::collectionChanged; // So we don't trigger -Woverloaded-virtual
+    GoogleSettings *settings() const Q_DECL_OVERRIDE;
+    QList< QUrl > scopes() const Q_DECL_OVERRIDE;
+
+protected:
+    // Freebusy
+    KDateTime lastCacheUpdate() const Q_DECL_OVERRIDE;
+    void canHandleFreeBusy(const QString &email) const Q_DECL_OVERRIDE;
+    void retrieveFreeBusy(const QString &email, const KDateTime &start, const KDateTime &end) Q_DECL_OVERRIDE;
+
+protected Q_SLOTS:
+    void retrieveCollections() Q_DECL_OVERRIDE;
+    void retrieveItems(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+
+    void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    void itemChanged(const Akonadi::Item &item, const QSet< QByteArray > &partIdentifiers) Q_DECL_OVERRIDE;
+    void itemRemoved(const Akonadi::Item &item) Q_DECL_OVERRIDE;
+    void itemMoved(const Akonadi::Item &item,
+                   const Akonadi::Collection &collectionSource,
+                   const Akonadi::Collection &collectionDestination) Q_DECL_OVERRIDE;
+
+    void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) Q_DECL_OVERRIDE;
+    void collectionChanged(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    void collectionRemoved(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+
+    void slotItemsRetrieved(KGAPI2::Job *job);
+    void slotCollectionsRetrieved(KGAPI2::Job *job);
+    void slotCalendarsRetrieved(KGAPI2::Job *job);
+    void slotRemoveTaskFetchJobFinished(KJob *job);
+    void slotDoRemoveTask(KJob *job);
+    void slotModifyTaskReparentFinished(KGAPI2::Job *job);
+    void slotTaskAddedSearchFinished(KJob *);
+    void slotCreateJobFinished(KGAPI2::Job *job);
+
+    void slotCanHandleFreeBusyJobFinished(KGAPI2::Job *job);
+    void slotRetrieveFreeBusyJobFinished(KGAPI2::Job *job);
+
+protected:
+    int runConfigurationDialog(WId windowId) Q_DECL_OVERRIDE;
+    void updateResourceName() Q_DECL_OVERRIDE;
+
+private:
+    QMap<QString, Akonadi::Collection> m_collections;
+    Akonadi::Collection m_rootCollection;
+
+};
+
+#endif // CALENDARRESOURCE_H
diff --git a/resources/google/calendar/defaultreminderattribute.cpp b/resources/google/calendar/defaultreminderattribute.cpp
new file mode 100644 (file)
index 0000000..962e207
--- /dev/null
@@ -0,0 +1,118 @@
+/*
+    Copyright (C) 2011-2013 Daniel Vrátil <dvratil@redhat.com>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "defaultreminderattribute.h"
+
+#include <QVariant>
+#include <QJsonDocument>
+
+#include <KGAPI/Calendar/Reminder>
+
+using namespace KGAPI2;
+
+DefaultReminderAttribute::DefaultReminderAttribute()
+{
+}
+
+Akonadi::Attribute *DefaultReminderAttribute::clone() const
+{
+    DefaultReminderAttribute *attr = new DefaultReminderAttribute();
+    attr->setReminders(m_reminders);
+
+    return attr;
+}
+
+void DefaultReminderAttribute::setReminders(const RemindersList &reminders)
+{
+    m_reminders = reminders;
+}
+
+void DefaultReminderAttribute::deserialize(const QByteArray &data)
+{
+    QJsonDocument json = QJsonDocument::fromJson(data);
+    if (json.isNull()) {
+        return;
+    }
+
+    QVariant var = json.toVariant();
+    QVariantList list;
+
+    list = var.toList();
+    Q_FOREACH (const QVariant &l, list) {
+        QVariantMap reminder = l.toMap();
+
+        KGAPI2::ReminderPtr rem(new KGAPI2::Reminder);
+
+        if (reminder[QStringLiteral("type")].toString() == QLatin1String("display")) {
+            rem->setType(KCalCore::Alarm::Display);
+        } else if (reminder[QStringLiteral("type")].toString() == QLatin1String("email")) {
+            rem->setType(KCalCore::Alarm::Email);
+        }
+
+        KCalCore::Duration offset(reminder[QStringLiteral("time")].toInt(), KCalCore::Duration::Seconds);
+        rem->setStartOffset(offset);
+
+        m_reminders << rem;
+    }
+}
+
+QByteArray DefaultReminderAttribute::serialized() const
+{
+    QVariantList list;
+    list.reserve(m_reminders.count());
+
+    Q_FOREACH (const ReminderPtr &rem, m_reminders) {
+        QVariantMap reminder;
+
+        if (rem->type() == KCalCore::Alarm::Display) {
+            reminder[QStringLiteral("type")] = QLatin1String("display");
+        } else if (rem->type() == KCalCore::Alarm::Email) {
+            reminder[QStringLiteral("type")] = QLatin1String("email");
+        }
+
+        reminder[QStringLiteral("time")] = rem->startOffset().asSeconds();
+
+        list << reminder;
+    }
+    QJsonDocument serialized = QJsonDocument::fromVariant(list);
+    return serialized.toJson();
+}
+
+KCalCore::Alarm::List DefaultReminderAttribute::alarms(KCalCore::Incidence *incidence) const
+{
+    KCalCore::Alarm::List alarms;
+    alarms.reserve(m_reminders.count());
+    Q_FOREACH (const ReminderPtr &reminder, m_reminders) {
+        KCalCore::Alarm::Ptr alarm(new KCalCore::Alarm(incidence));
+
+        alarm->setType(reminder->type());
+        alarm->setTime(incidence->dtStart());
+        alarm->setStartOffset(reminder->startOffset());
+        alarm->setEnabled(true);
+
+        alarms << alarm;
+    }
+
+    return alarms;
+}
+
+QByteArray DefaultReminderAttribute::type() const
+{
+    static const QByteArray sType("defaultReminders");
+    return sType;
+}
+
diff --git a/resources/google/calendar/defaultreminderattribute.h b/resources/google/calendar/defaultreminderattribute.h
new file mode 100644 (file)
index 0000000..dbc37b3
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+    Copyright (C) 2011-2013 Daniel Vrátil <dvratil@redhat.com>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef GOOGLE_CALENDAR_DEFAULTREMINDERATTRIBUTE_H
+#define GOOGLE_CALENDAR_DEFAULTREMINDERATTRIBUTE_H
+
+#include <AkonadiCore/Attribute>
+
+#include <KGAPI/Types>
+
+#include <KCalCore/Alarm>
+#include <KCalCore/Incidence>
+
+class DefaultReminderAttribute : public Akonadi::Attribute
+{
+public:
+    explicit DefaultReminderAttribute();
+
+    Attribute *clone() const Q_DECL_OVERRIDE;
+    void deserialize(const QByteArray &data) Q_DECL_OVERRIDE;
+    QByteArray serialized() const Q_DECL_OVERRIDE;
+    QByteArray type() const Q_DECL_OVERRIDE;
+
+    void setReminders(const KGAPI2::RemindersList &reminders);
+    KCalCore::Alarm::List alarms(KCalCore::Incidence *incidence) const;
+
+private:
+    KGAPI2::RemindersList m_reminders;
+};
+
+#endif // DEFAULTREMINDERATTRIBUTE_H
diff --git a/resources/google/calendar/googlecalendarresource.desktop b/resources/google/calendar/googlecalendarresource.desktop
new file mode 100644 (file)
index 0000000..66ad1f1
--- /dev/null
@@ -0,0 +1,90 @@
+[Desktop Entry]
+Name=Google Calendars and Tasks
+Name[bg]=Календари и задачи в Google
+Name[bs]=Google kalendari i zadaće
+Name[ca]=Calendaris i tasques de Google
+Name[ca@valencia]=Calendaris i tasques de Google
+Name[cs]=Kalendáře a úkoly Google
+Name[da]=Google kalendere og opgaver
+Name[de]=Google-Kalender und -Tasks
+Name[el]=Google ημερολόγια και εργασίες
+Name[en_GB]=Google Calendars and Tasks
+Name[es]=Calendarios Google y tareas
+Name[et]=Google'i kalendrid ja ülesanded
+Name[fi]=Google-kalenterit ja -tehtävät
+Name[fr]=Agendas et tâches Google
+Name[gl]=Google Calendars and Tasks
+Name[hu]=Google naptárak és feladatok
+Name[ia]=Calendarios e cargas de Google
+Name[it]=Calendari e Attività Google
+Name[kk]=Google күнтізбелер мен тапсырмалар
+Name[km]=ភារកិច្ច និង​ប្រតិទិន Google
+Name[ko]=Google 캘린더 및 할 일
+Name[lt]=Google kalendoriai ir užduotys
+Name[lv]=Google kalendāri un uzdevumi
+Name[nb]=Google kalendere og gjøremål
+Name[nds]=Google-Kalenners un -Opgaven
+Name[nl]=Google agenda's en taken
+Name[pl]=Kalendarze i zadania Google
+Name[pt]=Calendários e Tarefas do Google
+Name[pt_BR]=Calendários e tarefas do Google
+Name[ru]=Задачи и календари Google
+Name[sk]=Google kalendáre a úlohy
+Name[sl]=Koledarji in opravila Google
+Name[sr]=Гуглови календари и задаци
+Name[sr@ijekavian]=Гуглови календари и задаци
+Name[sr@ijekavianlatin]=Googleovi kalendari i zadaci
+Name[sr@latin]=Googleovi kalendari i zadaci
+Name[sv]=Google kalendrar och uppgifter
+Name[tr]=Google Takvimleri ve Görevleri
+Name[uk]=Календарі та записи завдань Google
+Name[x-test]=xxGoogle Calendars and Tasksxx
+Name[zh_CN]=Google 日历和任务
+Name[zh_TW]=Google 行事曆與工作
+Comment=Access your Google Calendars and Tasks from KDE
+Comment[bg]=Достъп до календарите и задачите ви в Google от KDE
+Comment[bs]=Pristupite svojim Google kalendaru i zadacima iz KDE
+Comment[ca]=Accediu als calendaris i tasques de Google des del KDE
+Comment[ca@valencia]=Accediu als calendaris i tasques de Google des del KDE
+Comment[da]=Tilgå dine Google kalendere og opgaver fra KDE
+Comment[de]=Greifen Sie in KDE auf Ihre Google-Kalender und -Tasks zu
+Comment[el]=Αποκτήστε πρόσβαση στα Google ημερολόγια και τις εργασίες σας από το KDE
+Comment[en_GB]=Access your Google Calendars and Tasks from KDE
+Comment[es]=Acceda a sus calendarios Google y tareas desde KDE
+Comment[et]=Oma Google'i kalendrite ja ülesannete kasutamine otse KDE-st
+Comment[fi]=Google-kalentereihin ja -tehtäviin pääsy KDE:sta
+Comment[fr]=Accès à vos agendas et listes de tâches Google depuis KDE
+Comment[gl]=Acceda aos seus calendarios e tarefas de Google desde KDE.
+Comment[hu]=A Google naptárának és feladatainak elérése a KDE-ből
+Comment[ia]=Accede a tu Calendarios  e Cargas de Google ab KDE
+Comment[it]=Accedi ai tuoi calendari e attività Google da KDE
+Comment[kk]=Google күнтізбелер мен тапсырмаларға KDE-ден қатынау
+Comment[km]=ដំណើរការ​ប្រតិទិន Google របស់​អ្នក និង​ភារកិច្ច​ពី KDE
+Comment[ko]=KDE에서 Google 캘린더 및 할 일에 접근하기
+Comment[lt]=Pasiekite savo Google kalendorius ir užduotis iš KDE
+Comment[lv]=Piekļūstiet saviem Google kalendāriem un uzdevumiem no KDE
+Comment[nb]=Bruk dine Google-kalendere og gjøremål fra KDE
+Comment[nds]=Ut KDE op Dien Google-Kalenners un -Opgaven togriepen
+Comment[nl]=Heb toegang tot uw Google agenda's en taken vanuit KDE 
+Comment[pl]=Uzyskaj dostęp do Kalendarzy i zadań Google z KDE
+Comment[pt]=Aceda aos seus calendários e tarefas da Google a partir do KDE
+Comment[pt_BR]=Acesse seus calendários e tarefas do Google a partir do KDE
+Comment[ru]=Доступ к задачам и календарям Google из KDE
+Comment[sk]=Pristupuje k vašim Google kalendárom a úlohám z KDE
+Comment[sl]=Dostopajte do svojih koledarjev in opravil Google
+Comment[sr]=Приступите својим календарима и задацима на Гуглу из КДЕ‑а
+Comment[sr@ijekavian]=Приступите својим календарима и задацима на Гуглу из КДЕ‑а
+Comment[sr@ijekavianlatin]=Pristupite svojim kalendarima i zadacima na Googleu iz KDE‑a
+Comment[sr@latin]=Pristupite svojim kalendarima i zadacima na Googleu iz KDE‑a
+Comment[sv]=Kom åt Google kalendrar och uppgifter från KDE
+Comment[tr]=Google Takvimlerinize ve Görevlerinize KDE'den erişin
+Comment[uk]=Доступ до ваших календарів і записів завдань Google з KDE
+Comment[x-test]=xxAccess your Google Calendars and Tasks from KDExx
+Comment[zh_CN]=在 KDE 中访问您的 Google 日历和任务
+Comment[zh_TW]=用 KDE 存取您的 Google 行事曆與工作
+Type=AkonadiResource
+Exec=akonadi_googlecalendar_resource
+X-Akonadi-MimeTypes=text/calendar,application/x-vnd.akonadi.calendar.event,application/x-vnd.akonadi.calendar.todo,application/x-vnd.akonadi.calendar.freebusy
+X-Akonadi-Capabilities=Resource,FreeBusyProvider
+X-Akonadi-Identifier=akonadi_googlecalendar_resource
+Icon=im-google
diff --git a/resources/google/calendar/settings.cpp b/resources/google/calendar/settings.cpp
new file mode 100644 (file)
index 0000000..8a2a713
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+    Copyright (C) 2011, 2012  Dan Vratil <dan@progdan.cz>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "settings.h"
+#include "settingsadaptor.h"
+
+#include <KWallet/Wallet>
+
+#include <QDBusConnection>
+
+class SettingsHelper
+{
+public:
+    SettingsHelper() : q(Q_NULLPTR)
+    {
+    }
+
+    ~SettingsHelper()
+    {
+        delete q;
+        q = Q_NULLPTR;
+    }
+
+    Settings *q;
+};
+
+Q_GLOBAL_STATIC(SettingsHelper, s_globalSettings)
+
+Settings::Settings():
+    GoogleSettings()
+{
+    Q_ASSERT(!s_globalSettings->q);
+    s_globalSettings->q = this;
+
+    new SettingsAdaptor(this);
+    QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"), this,
+            QDBusConnection::ExportAdaptors | QDBusConnection::ExportScriptableContents);
+}
+
+Settings *Settings::self()
+{
+    if (!s_globalSettings->q) {
+        new Settings;
+        s_globalSettings->q->load();
+    }
+
+    return s_globalSettings->q;
+
+}
diff --git a/resources/google/calendar/settings.h b/resources/google/calendar/settings.h
new file mode 100644 (file)
index 0000000..298eb92
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+        Copyright (C) 2011, 2012  Dan Vratil <dan@progdan.cz>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef GOOGLE_CALENDAR_SETTINGS_H
+#define GOOGLE_CALENDAR_SETTINGS_H
+
+#include "common/googlesettings.h"
+
+class Settings: public GoogleSettings
+{
+    Q_OBJECT
+    Q_CLASSINFO("D-Bus Interface", "org.kde.Akonadi.GoogleCalendar.ExtendedSettings")
+public:
+    Settings();
+    static Settings *self();
+
+};
+
+#endif // SETTINGS_H
diff --git a/resources/google/calendar/settingsbase.kcfg b/resources/google/calendar/settingsbase.kcfg
new file mode 100644 (file)
index 0000000..ac58395
--- /dev/null
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:kcfg="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
+                         http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
+  <kcfgfile/>
+  <group name="General">
+    <entry name="Account" type="String">
+    </entry>
+    <!-- AccountName and accountId are used only when using
+         KAccounts instead of native authentication //-->
+    <entry name="AccountName" type="String">
+    </entry>
+    <entry name="AccountId" type="Int">
+        <default>0</default>
+    </entry>
+
+    <entry name="Calendars" type="StringList">
+      <default></default>
+      <label>IDs of calendars in collection</label>
+    </entry>
+    <entry name="TaskLists" type="StringList">
+      <default></default>
+      <label>IDs of task lists in collection</label>
+    </entry>
+    <entry name="EventsSince" type="String">
+        <default></default>
+    </entry>
+    <entry name="EnableIntervalCheck" type="Bool">
+      <default>false</default>
+    </entry>
+    <entry name="IntervalCheckTime" type="Int">
+        <default>60</default>
+    </entry>
+  </group>
+</kcfg>
diff --git a/resources/google/calendar/settingsbase.kcfgc b/resources/google/calendar/settingsbase.kcfgc
new file mode 100644 (file)
index 0000000..8eda3d3
--- /dev/null
@@ -0,0 +1,7 @@
+File=settingsbase.kcfg
+ClassName=SettingsBase
+Mutators=true
+ItemAccessors=true
+SetUserTexts=true
+Singleton=false
+GlobalEnums=true
\ No newline at end of file
diff --git a/resources/google/calendar/settingsdialog.cpp b/resources/google/calendar/settingsdialog.cpp
new file mode 100644 (file)
index 0000000..bccd4b1
--- /dev/null
@@ -0,0 +1,227 @@
+/*
+    Copyright (C) 2011-2013  Daniel Vrátil <dvratil@redhat.com>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "settingsdialog.h"
+#include "settings.h"
+
+#include <KLocalizedString>
+#include <QListWidget>
+#include <QPushButton>
+#include <KDateComboBox>
+
+#include <QPixmap>
+#include <QIcon>
+#include <QGroupBox>
+#include <QLayout>
+#include <QLabel>
+#include <QtCore/QPointer>
+
+#include <KGAPI/Account>
+#include <KGAPI/AuthJob>
+#include <KGAPI/Calendar/Calendar>
+#include <KGAPI/Calendar/CalendarFetchJob>
+#include <KGAPI/Tasks/TaskList>
+#include <KGAPI/Tasks/TaskListFetchJob>
+
+using namespace KGAPI2;
+
+SettingsDialog::SettingsDialog(GoogleAccountManager *accountManager, WId windowId, GoogleResource *parent):
+    GoogleSettingsDialog(accountManager, windowId, parent)
+{
+    connect(this, &SettingsDialog::currentAccountChanged, this, &SettingsDialog::slotCurrentAccountChanged);
+
+    m_calendarsBox = new QGroupBox(i18n("Calendars"), this);
+    mainLayout()->addWidget(m_calendarsBox);
+
+    QVBoxLayout *vbox = new QVBoxLayout(m_calendarsBox);
+
+    m_calendarsList = new QListWidget(m_calendarsBox);
+    vbox->addWidget(m_calendarsList, 1);
+
+    m_reloadCalendarsBtn = new QPushButton(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Reload"), m_calendarsBox);
+    vbox->addWidget(m_reloadCalendarsBtn);
+    connect(m_reloadCalendarsBtn, &QPushButton::clicked, this, &SettingsDialog::slotReloadCalendars);
+
+    QHBoxLayout *hbox = new QHBoxLayout;
+    vbox->addLayout(hbox);
+
+    m_eventsLimitLabel = new QLabel(i18nc("Followed by a date picker widget", "&Fetch only new events since"), this);
+    hbox->addWidget(m_eventsLimitLabel);
+
+    m_eventsLimitCombo = new KDateComboBox(this);
+    m_eventsLimitLabel->setBuddy(m_eventsLimitCombo);
+    m_eventsLimitCombo->setMaximumDate(QDate::currentDate());
+    m_eventsLimitCombo->setMinimumDate(QDate::fromString(QStringLiteral("2000-01-01"), Qt::ISODate));
+    m_eventsLimitCombo->setOptions(KDateComboBox::EditDate | KDateComboBox::SelectDate |
+                                   KDateComboBox::DatePicker | KDateComboBox::WarnOnInvalid);
+    if (Settings::self()->eventsSince().isEmpty()) {
+        const QString ds = QStringLiteral("%1-01-01").arg(QString::number(QDate::currentDate().year() - 3));
+        m_eventsLimitCombo->setDate(QDate::fromString(ds, Qt::ISODate));
+    } else {
+        m_eventsLimitCombo->setDate(QDate::fromString(Settings::self()->eventsSince(), Qt::ISODate));
+    }
+    hbox->addWidget(m_eventsLimitCombo);
+
+    m_taskListsBox = new QGroupBox(i18n("Tasklists"), this);
+    mainLayout()->addWidget(m_taskListsBox);
+
+    vbox = new QVBoxLayout(m_taskListsBox);
+
+    m_taskListsList = new QListWidget(m_taskListsBox);
+    vbox->addWidget(m_taskListsList, 1);
+
+    m_reloadTaskListsBtn = new QPushButton(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Reload"), m_taskListsBox);
+    vbox->addWidget(m_reloadTaskListsBtn);
+    connect(m_reloadTaskListsBtn, &QPushButton::clicked, this, &SettingsDialog::slotReloadTaskLists);
+}
+
+SettingsDialog::~SettingsDialog()
+{
+}
+
+void SettingsDialog::saveSettings()
+{
+    const AccountPtr account = currentAccount();
+    if (!currentAccount()) {
+        Settings::self()->setAccount(QString());
+        Settings::self()->setCalendars(QStringList());
+        Settings::self()->setTaskLists(QStringList());
+        Settings::self()->setEventsSince(QString());
+        Settings::self()->save();
+        return;
+    }
+
+    Settings::self()->setAccount(account->accountName());
+
+    QStringList calendars;
+    for (int i = 0; i < m_calendarsList->count(); i++) {
+        QListWidgetItem *item = m_calendarsList->item(i);
+
+        if (item->checkState() == Qt::Checked) {
+            calendars.append(item->data(Qt::UserRole).toString());
+        }
+    }
+    Settings::self()->setCalendars(calendars);
+    if (m_eventsLimitCombo->isValid()) {
+        Settings::self()->setEventsSince(m_eventsLimitCombo->date().toString(Qt::ISODate));
+    }
+
+    QStringList taskLists;
+    for (int i = 0; i < m_taskListsList->count(); i++) {
+        QListWidgetItem *item = m_taskListsList->item(i);
+
+        if (item->checkState() == Qt::Checked) {
+            taskLists.append(item->data(Qt::UserRole).toString());
+        }
+    }
+    Settings::self()->setTaskLists(taskLists);
+
+    Settings::self()->save();
+}
+
+void SettingsDialog::slotCurrentAccountChanged(const QString &accountName)
+{
+    if (accountName.isEmpty()) {
+        return;
+    }
+
+    slotReloadCalendars();
+    slotReloadTaskLists();
+}
+
+void SettingsDialog::slotReloadCalendars()
+{
+    const AccountPtr account = currentAccount();
+    if (!account) {
+        return;
+    }
+
+    CalendarFetchJob *fetchJob = new CalendarFetchJob(account, this);
+    connect(fetchJob, &CalendarFetchJob::finished, this, &SettingsDialog::slotCalendarsRetrieved);
+
+    m_calendarsBox->setDisabled(true);
+    m_calendarsList->clear();
+}
+
+void SettingsDialog::slotReloadTaskLists()
+{
+    const AccountPtr account = currentAccount();
+    if (!account) {
+        return;
+    }
+
+    TaskListFetchJob *fetchJob = new TaskListFetchJob(account, this);
+    connect(fetchJob, &CalendarFetchJob::finished, this, &SettingsDialog::slotTaskListsRetrieved);
+
+    m_taskListsBox->setDisabled(true);
+    m_taskListsList->clear();
+}
+
+void SettingsDialog::slotCalendarsRetrieved(Job *job)
+{
+    if (!handleError(job) || !currentAccount()) {
+        m_calendarsBox->setEnabled(true);
+        return;
+    }
+
+    FetchJob *fetchJob = qobject_cast<FetchJob *>(job);
+    ObjectsList objects = fetchJob->items();
+
+    QStringList activeCalendars;
+    if (currentAccount()->accountName() == Settings::self()->account()) {
+        activeCalendars = Settings::self()->calendars();
+    }
+    Q_FOREACH (const ObjectPtr &object, objects) {
+        CalendarPtr calendar = object.dynamicCast<Calendar>();
+
+        QListWidgetItem *item = new QListWidgetItem(calendar->title());
+        item->setData(Qt::UserRole, calendar->uid());
+        item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable);
+        item->setCheckState((activeCalendars.isEmpty() || activeCalendars.contains(calendar->uid())) ? Qt::Checked : Qt::Unchecked);
+        m_calendarsList->addItem(item);
+
+    }
+
+    m_calendarsBox->setEnabled(true);
+}
+
+void SettingsDialog::slotTaskListsRetrieved(Job *job)
+{
+    if (!handleError(job) || !currentAccount()) {
+        m_taskListsBox->setEnabled(true);
+        return;
+    }
+
+    FetchJob *fetchJob = qobject_cast<FetchJob *>(job);
+    ObjectsList objects = fetchJob->items();
+
+    QStringList activeTaskLists;
+    if (currentAccount()->accountName() == Settings::self()->account()) {
+        activeTaskLists = Settings::self()->taskLists();
+    }
+    Q_FOREACH (const ObjectPtr &object, objects) {
+        TaskListPtr taskList = object.dynamicCast<TaskList>();
+
+        QListWidgetItem *item = new QListWidgetItem(taskList->title());
+        item->setData(Qt::UserRole, taskList->uid());
+        item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable);
+        item->setCheckState((activeTaskLists.isEmpty() || activeTaskLists.contains(taskList->uid())) ? Qt::Checked : Qt::Unchecked);
+        m_taskListsList->addItem(item);
+    }
+
+    m_taskListsBox->setEnabled(true);
+}
diff --git a/resources/google/calendar/settingsdialog.h b/resources/google/calendar/settingsdialog.h
new file mode 100644 (file)
index 0000000..bdd2d3e
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+    Copyright (C) 2011-2013  Daniel Vrátil <dvratil@redhat.com>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef GOOGLE_CALENDAR_SETTINGSDIALOG_H
+#define GOOGLE_CALENDAR_SETTINGSDIALOG_H
+
+#include "common/googlesettingsdialog.h"
+
+class QListWidget;
+class QLabel;
+class KDateComboBox;
+
+class SettingsDialog : public GoogleSettingsDialog
+{
+    Q_OBJECT
+public:
+    explicit SettingsDialog(GoogleAccountManager *accountManager, WId windowId, GoogleResource *parent);
+    ~SettingsDialog();
+
+private Q_SLOTS:
+    void slotReloadCalendars();
+    void slotReloadTaskLists();
+    void slotCurrentAccountChanged(const QString &accountName);
+
+    void slotTaskListsRetrieved(KGAPI2::Job *job);
+    void slotCalendarsRetrieved(KGAPI2::Job *job);
+
+    void saveSettings() Q_DECL_OVERRIDE;
+
+private:
+    QGroupBox *m_calendarsBox;
+    QListWidget *m_calendarsList;
+    QPushButton *m_reloadCalendarsBtn;
+    QLabel *m_eventsLimitLabel;
+    KDateComboBox *m_eventsLimitCombo;
+
+    QGroupBox *m_taskListsBox;
+    QListWidget *m_taskListsList;
+    QPushButton *m_reloadTaskListsBtn;
+
+};
+
+#endif // SETTINGSDIALOG_H
diff --git a/resources/google/common/googleaccountmanager.cpp b/resources/google/common/googleaccountmanager.cpp
new file mode 100644 (file)
index 0000000..97e043b
--- /dev/null
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2013  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+#include "googleaccountmanager.h"
+
+#include <QtCore/QTimer>
+
+#include <KWallet/kwallet.h>
+#include <QDebug>
+#include <KWindowSystem>
+
+#include <KGAPI/Account>
+
+#define WALLET_FOLDER QStringLiteral("Akonadi Google")
+
+using namespace KGAPI2;
+
+GoogleAccountManager::GoogleAccountManager(QObject *parent):
+    QObject(parent),
+    m_isReady(false)
+{
+    QMetaObject::invokeMethod(this, "initManager", Qt::QueuedConnection);
+}
+
+GoogleAccountManager::~GoogleAccountManager()
+{
+    delete m_wallet;
+}
+
+bool GoogleAccountManager::isReady() const
+{
+    return m_isReady;
+}
+
+void GoogleAccountManager::initManager()
+{
+    delete m_wallet;
+
+    // FIXME: Don't use synchronous wallet
+    // With asynchronous wallet however we are unable to read any data from it
+    // in when slotWalletOpened() is called on walletOpened() signal
+    m_wallet = KWallet::Wallet::openWallet(KWallet::Wallet::NetworkWallet(),
+                                           0, KWallet::Wallet::Synchronous);
+    slotWalletOpened(m_wallet != Q_NULLPTR);
+//     connect( m_wallet, SIGNAL(walletOpened(bool)),
+//              this, SLOT(slotWalletOpened(bool)) );
+    if (m_wallet) {
+        connect(m_wallet.data(), &KWallet::Wallet::folderUpdated, this, &GoogleAccountManager::slotFolderUpdated);
+        connect(m_wallet.data(), &KWallet::Wallet::walletClosed, this, &GoogleAccountManager::slotWalletClosed);
+    }
+}
+
+void GoogleAccountManager::slotWalletOpened(bool success)
+{
+    if (!success) {
+        qWarning() << "Failed to open wallet";
+        Q_EMIT managerReady(false);
+        return;
+    }
+
+    if (!m_wallet->hasFolder(WALLET_FOLDER)) {
+        if (!m_wallet->createFolder(WALLET_FOLDER)) {
+            qWarning() << "Failed to create KWallet folder " << WALLET_FOLDER;
+            Q_EMIT managerReady(false);
+            return;
+        }
+    }
+
+    if (!m_wallet->setFolder(WALLET_FOLDER)) {
+        qWarning() << "Failed to open KWallet folder" << WALLET_FOLDER;
+        Q_EMIT managerReady(false);
+        return;
+    }
+
+    // Populate the cache now
+    QStringList accountNames = m_wallet->entryList();
+    Q_FOREACH (const QString &accountName, accountNames) {
+        m_accounts[accountName] = findAccountInWallet(accountName);
+    }
+
+    m_isReady = true;
+    Q_EMIT managerReady(true);
+}
+
+void GoogleAccountManager::slotWalletClosed()
+{
+    m_isReady = false;
+    delete m_wallet;
+}
+
+void GoogleAccountManager::slotFolderUpdated(const QString &folder)
+{
+    // We are interested only in the "Akonadi Google" folder
+    if (folder != WALLET_FOLDER) {
+        return;
+    }
+
+    QStringList walletEntries = m_wallet->entryList();
+
+    Q_FOREACH (const AccountPtr &account, m_accounts) {
+        AccountPtr changedAccount = findAccountInWallet(account->accountName());
+        if (changedAccount.isNull()) {
+            walletEntries.removeOne(account->accountName());
+            m_accounts.remove(account->accountName());
+            Q_EMIT accountRemoved(account->accountName());
+            continue;
+        }
+
+        if ((account->accessToken() != changedAccount->accessToken()) ||
+                (account->refreshToken() != changedAccount->refreshToken()) ||
+                (account->scopes() != changedAccount->scopes())) {
+
+            walletEntries.removeOne(account->accountName());
+            m_accounts[account->accountName()] = changedAccount;
+            Q_EMIT accountChanged(changedAccount);
+        }
+    }
+
+    Q_FOREACH (const QString &accountName, walletEntries) {
+        const AccountPtr newAccount = findAccountInWallet(accountName);
+
+        m_accounts[newAccount->accountName()] = newAccount;
+        Q_EMIT accountAdded(newAccount);
+    }
+}
+
+AccountPtr GoogleAccountManager::findAccount(const QString &accountName) const
+{
+    if (!m_isReady) {
+        qWarning() << "Manager is not ready!";
+        return AccountPtr();
+    }
+
+    if (m_accounts.contains(accountName)) {
+        return m_accounts[accountName];
+    }
+
+    AccountPtr account = findAccountInWallet(accountName);
+    if (account.isNull()) {
+        return AccountPtr();
+    }
+
+    m_accounts[accountName] = account;
+    return account;
+}
+
+AccountPtr GoogleAccountManager::findAccountInWallet(const QString &accountName) const
+{
+    if (!m_wallet->entryList().contains(accountName)) {
+        qDebug() << "Account" << accountName << "not found in KWallet";
+        return AccountPtr();
+    }
+
+    QMap<QString, QString> map;
+    m_wallet->readMap(accountName, map);
+
+    const QStringList scopes = map[QStringLiteral("scopes")].split(QLatin1Char(','), QString::SkipEmptyParts);
+    QList<QUrl> scopeUrls;
+    scopeUrls.reserve(scopes.count());
+    Q_FOREACH (const QString &scope, scopes) {
+        scopeUrls << QUrl(scope);
+    }
+    AccountPtr account(new Account(accountName,
+                                   map[QStringLiteral("accessToken")],
+                                   map[QStringLiteral("refreshToken")],
+                                   scopeUrls));
+
+    return account;
+}
+
+bool GoogleAccountManager::storeAccount(const AccountPtr &account)
+{
+    if (!m_isReady) {
+        qWarning() << "Manager is not ready!";
+        return false;
+    }
+
+    QStringList scopes;
+    const QList<QUrl> urlScopes = account->scopes();
+    scopes.reserve(urlScopes.count());
+    Q_FOREACH (const QUrl &url, urlScopes) {
+        scopes << url.toString();
+    }
+
+    QMap<QString, QString> map;
+    map[QStringLiteral("accessToken")] = account->accessToken();
+    map[QStringLiteral("refreshToken")] = account->refreshToken();
+    map[QStringLiteral("scopes")] = scopes.join(QStringLiteral(","));
+
+    if (m_wallet->writeMap(account->accountName(), map) == 0) {
+        m_accounts[account->accountName()] = account;
+        return true;
+    }
+
+    return false;
+}
+
+bool GoogleAccountManager::removeAccount(const QString &accountName)
+{
+    if (!m_isReady) {
+        qWarning() << "Manager is not ready";
+        return false;
+    }
+
+    if (!m_accounts.contains(accountName)) {
+        return true;
+    }
+
+    if (m_wallet->removeEntry(accountName) != 0) {
+        qWarning() << "Failed to remove account from KWallet";
+        return false;
+    }
+
+    m_accounts.remove(accountName);
+    return true;
+}
+
+AccountsList GoogleAccountManager::listAccounts() const
+{
+    if (!m_isReady) {
+        qWarning() << "Manager is not ready";
+        return AccountsList();
+    }
+
+    return m_accounts.values();
+}
+
+void GoogleAccountManager::cleanup(const QString &accountName)
+{
+    removeAccount(accountName);
+}
+
diff --git a/resources/google/common/googleaccountmanager.h b/resources/google/common/googleaccountmanager.h
new file mode 100644 (file)
index 0000000..7031e27
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2013  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+#ifndef GOOGLEACCOUNTMANAGER_H
+#define GOOGLEACCOUNTMANAGER_H
+
+#include <QtCore/QObject>
+#include <QtCore/QPointer>
+#include <QtCore/QMap>
+
+#include <KGAPI/Types>
+#include <KGAPI/Account>
+
+namespace KWallet
+{
+class Wallet;
+}
+
+class GoogleAccountManager : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit GoogleAccountManager(QObject *parent = Q_NULLPTR);
+    virtual ~GoogleAccountManager();
+
+    bool isReady() const;
+
+    bool storeAccount(const KGAPI2::AccountPtr &account);
+    KGAPI2::AccountPtr findAccount(const QString &accountName) const;
+    bool removeAccount(const QString &accountName);
+    KGAPI2::AccountsList listAccounts() const;
+
+    void cleanup(const QString &accountName);
+
+Q_SIGNALS:
+    void managerReady(bool ready);
+    void accountAdded(const KGAPI2::AccountPtr &account);
+    void accountChanged(const KGAPI2::AccountPtr &account);
+    void accountRemoved(const QString &accountName);
+
+private Q_SLOTS:
+    void initManager();
+    void slotWalletOpened(bool success);
+    void slotWalletClosed();
+    void slotFolderUpdated(const QString &folder);
+    KGAPI2::AccountPtr findAccountInWallet(const QString &accountName) const;
+
+private:
+    bool m_isReady;
+    QPointer<KWallet::Wallet> m_wallet;
+    mutable QMap<QString, KGAPI2::AccountPtr> m_accounts;
+};
+
+#endif // GOOGLEACCOUNTMANAGER_H
diff --git a/resources/google/common/googleresource.cpp b/resources/google/common/googleresource.cpp
new file mode 100644 (file)
index 0000000..cd499cb
--- /dev/null
@@ -0,0 +1,439 @@
+/*
+   Copyright (C) 2011-2013 Daniel Vrátil <dvratil@redhat.com>
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "googleresource.h"
+#include "googlesettings.h"
+
+#include <AkonadiCore/ChangeRecorder>
+#include <AkonadiCore/ItemFetchScope>
+#include <AkonadiCore/CollectionFetchScope>
+
+#include <QDialog>
+#include <KLocalizedString>
+
+#include <KGAPI/Account>
+#include <KGAPI/AccountInfoFetchJob>
+#include <KGAPI/AccountInfo>
+#include <KGAPI/AuthJob>
+
+#ifdef HAVE_ACCOUNTS
+#include "../../shared/singlefileresource/getcredentialsjob.h"
+#endif
+
+#define ACCESS_TOKEN_PROPERTY "AccessToken"
+
+Q_DECLARE_METATYPE(KGAPI2::Job *)
+
+using namespace KGAPI2;
+using namespace Akonadi;
+
+GoogleResource::GoogleResource(const QString &id):
+    ResourceBase(id),
+    AgentBase::ObserverV2(),
+    m_isConfiguring(false)
+{
+    //QT5 KLocalizedString::global()->insertCatalog( QStringLiteral("akonadi_google_resource") );
+    connect(this, &GoogleResource::abortRequested, this, &GoogleResource::slotAbortRequested);
+    connect(this, &GoogleResource::reloadConfiguration, this, &GoogleResource::reloadConfig);
+
+    setNeedsNetwork(true);
+
+    changeRecorder()->itemFetchScope().fetchFullPayload(true);
+    changeRecorder()->itemFetchScope().setAncestorRetrieval(ItemFetchScope::All);
+    changeRecorder()->fetchCollection(true);
+    changeRecorder()->collectionFetchScope().setAncestorRetrieval(CollectionFetchScope::All);
+
+    m_accountMgr = new GoogleAccountManager(this);
+    connect(m_accountMgr, &GoogleAccountManager::accountChanged, this, &GoogleResource::slotAccountChanged);
+    connect(m_accountMgr, &GoogleAccountManager::accountRemoved, this, &GoogleResource::slotAccountRemoved);
+    connect(m_accountMgr, &GoogleAccountManager::managerReady, this, &GoogleResource::slotAccountManagerReady);
+
+    Q_EMIT status(NotConfigured, i18n("Waiting for KWallet..."));
+}
+
+GoogleResource::~GoogleResource()
+{
+}
+
+void GoogleResource::cleanup()
+{
+    accountManager()->cleanup(settings()->account());
+    Akonadi::AgentBase::cleanup();
+}
+
+AccountPtr GoogleResource::account() const
+{
+    return m_account;
+}
+
+GoogleAccountManager *GoogleResource::accountManager() const
+{
+    return m_accountMgr;
+}
+
+void GoogleResource::aboutToQuit()
+{
+    slotAbortRequested();
+}
+
+void GoogleResource::abort()
+{
+    cancelTask(i18n("Aborted"));
+}
+
+void GoogleResource::slotAbortRequested()
+{
+    abort();
+}
+
+void GoogleResource::configure(WId windowId)
+{
+    if (!m_accountMgr->isReady() || m_isConfiguring) {
+        Q_EMIT configurationDialogAccepted();
+        return;
+    }
+
+    m_isConfiguring = true;
+    if (runConfigurationDialog(windowId) == QDialog::Accepted) {
+        updateResourceName();
+
+        Q_EMIT configurationDialogAccepted();
+
+        m_account = accountManager()->findAccount(settings()->account());
+        if (m_account.isNull()) {
+            Q_EMIT status(NotConfigured, i18n("Configured account does not exist"));
+            m_isConfiguring = false;
+            return;
+        }
+
+        Q_EMIT status(Idle, i18nc("@info:status", "Ready"));
+        synchronize();
+    } else {
+        updateResourceName();
+
+        Q_EMIT configurationDialogRejected();
+    }
+    m_isConfiguring = false;
+}
+
+void GoogleResource::updateAccountToken(const AccountPtr &account, KGAPI2::Job *restartJob)
+{
+    if (accountId() > 0) {
+        configureKAccounts(accountId(), restartJob);
+    } else if (!settings()->account().isEmpty()) {
+        AuthJob *authJob = new AuthJob(account, settings()->clientId(), settings()->clientSecret(), this);
+        authJob->setProperty(JOB_PROPERTY, QVariant::fromValue(restartJob));
+        connect(authJob, &AuthJob::finished, this, &GoogleResource::slotAuthJobFinished);
+    }
+}
+
+void GoogleResource::reloadConfig()
+{
+    const QString accountName = settings()->account();
+
+    if (accountId() > 0) {
+        if (!configureKAccounts(accountId())) {
+            Q_EMIT status(Broken);
+            return;
+        }
+    } else if (!accountName.isEmpty()) {
+        if (!configureKGAPIAccount(m_accountMgr->findAccount(accountName))) {
+            Q_EMIT status(NotConfigured, i18n("Configured account does not exist"));
+            return;
+        }
+    } else {
+        Q_EMIT status(NotConfigured);
+        return;
+    }
+
+    Q_EMIT status(Idle, i18nc("@info:status", "Ready"));
+}
+
+bool GoogleResource::configureKAccounts(int accountId, KGAPI2::Job *restartJob)
+{
+    if (accountId == 0) {
+        return false;
+    }
+#ifdef HAVE_ACCOUNTS
+    GetCredentialsJob *gc = new GetCredentialsJob(accountId, this);
+    gc->setProperty(JOB_PROPERTY, QVariant::fromValue(restartJob));
+    connect(gc, &GetCredentialsJob::finished, this, &GoogleResource::slotKAccountsCredentialsReceived);
+    gc->start();
+    // SUCKS!
+    return true;
+#else
+    Q_UNUSED(restartJob);
+    return false;
+#endif
+}
+
+#ifdef HAVE_ACCOUNTS
+void GoogleResource::slotKAccountsCredentialsReceived(KJob *job)
+{
+    if (job->error()) {
+        Q_EMIT status(Broken);
+        // FIXME: Fallback to KGAPI account?
+        return;
+    }
+
+    GetCredentialsJob *gc = qobject_cast<GetCredentialsJob *>(job);
+    const QVariantMap data = gc->credentialsData();
+    const QString accessToken = data.value(QStringLiteral("AccessToken")).toString();
+
+    // Createa temporary account that we use to fetch full user name
+    KGAPI2::AccountPtr account(new KGAPI2::Account);
+    account->setAccessToken(accessToken);
+    account->setScopes(scopes());
+
+    KGAPI2::Job *otherJob = 0;
+    if (!job->property(JOB_PROPERTY).isNull()) {
+        otherJob = job->property(JOB_PROPERTY).value<KGAPI2::Job *>();
+    }
+
+    if (settings()->accountName().isEmpty()) {
+        account->setAccountName(i18n("Unknown Account"));
+        AccountInfoFetchJob *aiJob = new AccountInfoFetchJob(account, this);
+        aiJob->setProperty(ACCESS_TOKEN_PROPERTY, accessToken);
+        if (otherJob) {
+            aiJob->setProperty(JOB_PROPERTY, QVariant::fromValue(otherJob));
+        }
+        connect(aiJob, &AccountInfoFetchJob::finished, this, &GoogleResource::slotKAccountsAccountInfoReceived);
+    } else {
+        m_account = AccountPtr(new Account(settings()->accountName(),
+                                           accessToken));
+        finishKAccountsAuthentication(otherJob);
+    }
+}
+
+void GoogleResource::slotKAccountsAccountInfoReceived(KGAPI2::Job *job)
+{
+    if (!handleError(job)) {
+        Q_EMIT error(job->errorString());
+        cancelTask(i18n("Failed to refresh tokens"));
+        return;
+    }
+
+    AccountInfoFetchJob *aiJob = qobject_cast<AccountInfoFetchJob *>(job);
+    Q_ASSERT(aiJob);
+    aiJob->deleteLater();
+
+    const AccountPtr account = job->account();
+
+    if (aiJob->items().count() != 1) {
+        qWarning() << "AccountInfoFetchJob returned unexpected amount of results";
+        Q_EMIT error(i18n("Invalid reply"));
+        cancelTask(i18n("Failed to refresh tokens"));
+        return;
+    }
+
+    AccountInfoPtr info = aiJob->items().at(0).dynamicCast<AccountInfo>();
+    settings()->setAccountName(info->email());
+    m_account = AccountPtr(new Account(info->email(),
+                                       aiJob->property(ACCESS_TOKEN_PROPERTY).toString()));
+    settings()->writeConfig();
+
+    KGAPI2::Job *otherJob = 0;
+    if (job->property(JOB_PROPERTY).isNull()) {
+        otherJob = job->property(JOB_PROPERTY).value<KGAPI2::Job *>();
+    }
+
+    finishKAccountsAuthentication(otherJob);
+}
+
+void GoogleResource::finishKAccountsAuthentication(KGAPI2::Job *job)
+{
+    updateResourceName();
+    Q_EMIT status(Idle, i18nc("@info:status", "Ready"));
+
+    if (job) {
+        job->setAccount(m_account);
+        job->restart();
+    } else {
+        synchronize();
+    }
+}
+#endif // HAVE_ACCOUNTS
+
+bool GoogleResource::configureKGAPIAccount(const AccountPtr &account)
+{
+    m_account = account;
+    return !m_account.isNull();
+}
+
+void GoogleResource::slotAccountManagerReady(bool ready)
+{
+    // If the resource have already been configured for KAccounts, then use that
+    if (accountId() > 0) {
+        return;
+    }
+
+    qDebug() << ready;
+    if (!ready) {
+        Q_EMIT status(Broken, i18n("Can't access KWallet"));
+        return;
+    }
+
+    const QString accountName = settings()->account();
+    if (accountName.isEmpty()) {
+        Q_EMIT status(NotConfigured);
+        return;
+    }
+
+    m_account = m_accountMgr->findAccount(accountName);
+    if (m_account.isNull()) {
+        Q_EMIT status(NotConfigured, i18n("Configured account does not exist"));
+        return;
+    }
+
+    Q_EMIT status(Idle, i18nc("@info:status", "Ready"));
+    synchronize();
+}
+
+void GoogleResource::slotAccountChanged(const AccountPtr &account)
+{
+    // We don't care when using KAccounts
+    if (accountId() > 0) {
+        return;
+    }
+
+    m_account = account;
+}
+
+void GoogleResource::slotAccountRemoved(const QString &accountName)
+{
+    // We don't care when using KAccounts
+    if (accountId() > 0) {
+        return;
+    }
+
+    if (m_account && m_account->accountName() != accountName) {
+        return;
+    }
+
+    Q_EMIT status(NotConfigured, i18n("Configured account has been removed"));
+    m_account.clear();
+    settings()->setAccount(QString());
+}
+
+bool GoogleResource::handleError(KGAPI2::Job *job, bool _cancelTask)
+{
+    if ((job->error() == KGAPI2::NoError) || (job->error() == KGAPI2::OK)) {
+        return true;
+    }
+
+    if (job->error() == KGAPI2::Unauthorized) {
+        qDebug() << job << job->errorString();
+
+        const QList<QUrl> resourceScopes = scopes();
+        Q_FOREACH (const QUrl &scope, resourceScopes) {
+            if (!m_account->scopes().contains(scope)) {
+                m_account->addScope(scope);
+            }
+        }
+
+        updateAccountToken(m_account, job);
+        return false;
+    }
+
+    if (_cancelTask) {
+        cancelTask(job->errorString());
+    }
+    job->deleteLater();
+    return false;
+}
+
+bool GoogleResource::canPerformTask()
+{
+    if (!m_account && accountId() == 0) {
+        cancelTask(i18nc("@info:status", "Resource is not configured"));
+        Q_EMIT status(NotConfigured, i18nc("@info:status", "Resource is not configured"));
+        return false;
+    }
+
+    return true;
+}
+
+void GoogleResource::slotAuthJobFinished(KGAPI2::Job *job)
+{
+    qDebug();
+
+    if (job->error() != KGAPI2::NoError) {
+        cancelTask(i18n("Failed to refresh tokens"));
+        return;
+    }
+
+    AuthJob *authJob = qobject_cast<AuthJob *>(job);
+    m_account = authJob->account();
+    if (!m_accountMgr->storeAccount(m_account)) {
+        qWarning() << "Failed to store account in KWallet";
+    }
+
+    KGAPI2::Job *otherJob = job->property(JOB_PROPERTY).value<KGAPI2::Job *>();
+    if (otherJob) {
+        otherJob->setAccount(m_account);
+        otherJob->restart();
+    }
+
+    job->deleteLater();
+}
+
+void GoogleResource::slotGenericJobFinished(KGAPI2::Job *job)
+{
+    if (!handleError(job)) {
+        return;
+    }
+
+    const Item item = job->property(ITEM_PROPERTY).value<Item>();
+    const Collection collection = job->property(COLLECTION_PROPERTY).value<Collection>();
+    if (item.isValid()) {
+        changeCommitted(item);
+    } else if (collection.isValid()) {
+        changeCommitted(collection);
+    } else {
+        taskDone();
+    }
+
+    Q_EMIT status(Idle, i18nc("@info:status", "Ready"));
+
+    job->deleteLater();
+}
+
+void GoogleResource::emitPercent(KGAPI2::Job *job, int processedItems, int totalItems)
+{
+    Q_UNUSED(job);
+
+    Q_EMIT percent(((float) processedItems / (float) totalItems) * 100);
+}
+
+bool GoogleResource::retrieveItem(const Item &item, const QSet< QByteArray > &parts)
+{
+    Q_UNUSED(parts);
+
+    /* We don't support fetching parts, the item is already fully stored. */
+    itemRetrieved(item);
+
+    return true;
+}
+
+int GoogleResource::accountId() const
+{
+#ifdef HAVE_ACCOUNTS
+    return settings()->accountId();
+#else
+    return 0;
+#endif
+}
diff --git a/resources/google/common/googleresource.h b/resources/google/common/googleresource.h
new file mode 100644 (file)
index 0000000..718c865
--- /dev/null
@@ -0,0 +1,129 @@
+/*
+   Copyright (C) 2011-2013 Daniel Vrátil <dvratil@redhat.com>
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef GOOGLERESOURCE_H
+#define GOOGLERESOURCE_H
+
+#include <AkonadiAgentBase/ResourceBase>
+#include <AkonadiAgentBase/AgentBase>
+
+#include <qwindowdefs.h>
+
+#include "googleaccountmanager.h"
+
+#include <KGAPI/Types>
+
+#include <KLocalizedString>
+
+#define ITEM_PROPERTY "_AkonadiItem"
+#define ITEMLIST_PROPERTY "_AkonadiItemList"
+#define COLLECTION_PROPERTY "_AkonadiCollection"
+#define JOB_PROPERTY "_KGAPI2Job"
+
+namespace KGAPI2
+{
+class Job;
+}
+
+class GoogleSettings;
+
+class GoogleResource : public Akonadi::ResourceBase,
+    public Akonadi::AgentBase::ObserverV2
+{
+    Q_OBJECT
+
+public:
+    explicit GoogleResource(const QString &id);
+    virtual ~GoogleResource();
+
+    virtual GoogleSettings *settings() const = 0;
+    virtual QList<QUrl> scopes() const = 0;
+
+    void cleanup() Q_DECL_OVERRIDE;
+
+public Q_SLOTS:
+    void configure(WId windowId) Q_DECL_OVERRIDE;
+
+    void reloadConfig();
+
+protected Q_SLOTS:
+    bool retrieveItem(const Akonadi::Item &item, const QSet< QByteArray > &parts) Q_DECL_OVERRIDE;
+
+    bool handleError(KGAPI2::Job *job, bool cancelTask = true);
+
+    virtual void slotAuthJobFinished(KGAPI2::Job *job);
+    virtual void slotGenericJobFinished(KGAPI2::Job *job);
+
+    void emitPercent(KGAPI2::Job *job, int processedCount, int totalCount);
+
+    virtual void slotAbortRequested();
+    virtual void slotAccountManagerReady(bool success);
+    virtual void slotAccountChanged(const KGAPI2::AccountPtr &account);
+    virtual void slotAccountRemoved(const QString &accountName);
+
+#ifdef HAVE_ACCOUNTS
+    void slotKAccountsCredentialsReceived(KJob *job);
+    void slotKAccountsAccountInfoReceived(KGAPI2::Job *job);
+    void finishKAccountsAuthentication(KGAPI2::Job *job);
+#endif // HAVE_ACCOUNTS
+
+protected:
+    bool configureKAccounts(int accountId, KGAPI2::Job *restartJob = Q_NULLPTR);
+    bool configureKGAPIAccount(const KGAPI2::AccountPtr &account);
+    void updateAccountToken(const KGAPI2::AccountPtr &account, KGAPI2::Job *restartJob = Q_NULLPTR);
+
+    template <typename T>
+    bool canPerformTask(const Akonadi::Item &item, const QString &mimeType = QString())
+    {
+        if (item.isValid() && !item.hasPayload<T>()) {
+            cancelTask(i18n("Invalid item payload."));
+            return false;
+        } else if (item.isValid() && mimeType != item.mimeType()) {
+            cancelTask(i18n("Invalid payload mimetype. Expected %1, found %2", mimeType, item.mimeType()));
+            return false;
+        }
+
+        return canPerformTask();
+    }
+
+    bool canPerformTask();
+
+    KGAPI2::AccountPtr account() const;
+    /**
+     * KAccounts support abstraction.
+     *
+     * Returns 0 when compiled without KAccounts or not configured for KAccounts
+     */
+    int accountId() const;
+
+    GoogleAccountManager *accountManager() const;
+
+    void aboutToQuit() Q_DECL_OVERRIDE;
+
+    virtual int runConfigurationDialog(WId windowId) = 0;
+    virtual void updateResourceName() = 0;
+
+private:
+    void abort();
+
+    bool m_isConfiguring;
+    GoogleAccountManager *m_accountMgr;
+    KGAPI2::AccountPtr m_account;
+
+};
+
+#endif // GOOGLERESOURCE_H
diff --git a/resources/google/common/googlesettings.cpp b/resources/google/common/googlesettings.cpp
new file mode 100644 (file)
index 0000000..f0d9347
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+    Copyright (C) 2011-2013  Dan Vratil <dan@progdan.cz>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "googlesettings.h"
+#include "settingsbase.h"
+
+GoogleSettings::GoogleSettings():
+    m_winId(0)
+{
+}
+
+QString GoogleSettings::clientId() const
+{
+    return QStringLiteral("554041944266.apps.googleusercontent.com");
+}
+
+QString GoogleSettings::clientSecret() const
+{
+    return QStringLiteral("mdT1DjzohxN3npUUzkENT0gO");
+}
+
+void GoogleSettings::setWindowId(WId id)
+{
+    m_winId = id;
+}
+
+void GoogleSettings::setResourceId(const QString &resourceIdentificator)
+{
+    m_resourceId = resourceIdentificator;
+}
+
+QString GoogleSettings::account() const
+{
+    return SettingsBase::account();
+}
+
+void GoogleSettings::setAccount(const QString &account)
+{
+    SettingsBase::setAccount(account);
+}
+
diff --git a/resources/google/common/googlesettings.h b/resources/google/common/googlesettings.h
new file mode 100644 (file)
index 0000000..42fe789
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+   Copyright (C) 2013 Daniel Vrátil <dvratil@redhat.com>
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef GOOGLESETTINGS_H
+#define GOOGLESETTINGS_H
+
+#include "settingsbase.h"
+
+#include <QtCore/QObject>
+#include <qwindowdefs.h>
+
+/**
+ * @brief Settings object
+ *
+ * Provides read-only access to application clientId and
+ * clientSecret and read-write access to accessToken and
+ * refreshToken.
+ */
+class GoogleSettings: public SettingsBase
+{
+    Q_OBJECT
+
+public:
+    GoogleSettings();
+    void setWindowId(WId id);
+    void setResourceId(const QString &resourceIdentifier);
+
+    QString appId() const;
+
+    QString clientId() const;
+    QString clientSecret() const;
+
+    virtual QString account() const;
+    virtual void setAccount(const QString &account);
+
+private:
+    WId m_winId;
+    QString m_resourceId;
+
+};
+
+#endif // GOOGLESETTINGS_H
diff --git a/resources/google/common/googlesettingsdialog.cpp b/resources/google/common/googlesettingsdialog.cpp
new file mode 100644 (file)
index 0000000..0607a76
--- /dev/null
@@ -0,0 +1,267 @@
+/*
+   Copyright (C) 2013 Daniel Vrátil <dvratil@redhat.com>
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "googlesettingsdialog.h"
+#include "googleaccountmanager.h"
+#include "googlesettings.h"
+#include "googleresource.h"
+#include "settings.h"
+
+#include <QGroupBox>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QSpinBox>
+#include <QCheckBox>
+
+#include <KLocalizedString>
+#include <KComboBox>
+#include <QPushButton>
+#include <QDebug>
+#include <KMessageBox>
+#include <KWindowSystem>
+#include <KPluralHandlingSpinBox>
+
+#include <KGAPI/Account>
+#include <KGAPI/AuthJob>
+#include <QDialogButtonBox>
+#include <KConfigGroup>
+#include <QVBoxLayout>
+
+Q_DECLARE_METATYPE(KGAPI2::Job *)
+
+using namespace KGAPI2;
+
+GoogleSettingsDialog::GoogleSettingsDialog(GoogleAccountManager *accountManager, WId wId, GoogleResource *parent):
+    QDialog(),
+    m_parentResource(parent),
+    m_accountManager(accountManager)
+{
+    KWindowSystem::setMainWindow(this, wId);
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    QVBoxLayout *topLayout = new QVBoxLayout;
+    setLayout(topLayout);
+    QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
+    okButton->setDefault(true);
+    okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
+    connect(buttonBox, &QDialogButtonBox::accepted, this, &GoogleSettingsDialog::slotSaveSettings);
+    connect(buttonBox, &QDialogButtonBox::rejected, this, &GoogleSettingsDialog::reject);
+
+    QWidget *widget = new QWidget(this);
+    topLayout->addWidget(widget);
+    topLayout->addWidget(buttonBox);
+    QVBoxLayout *mainLayout = new QVBoxLayout(widget);
+    m_mainLayout = mainLayout;
+
+    m_accGroupBox = new QGroupBox(i18n("Accounts"), this);
+    mainLayout->addWidget(m_accGroupBox);
+    QHBoxLayout *accLayout = new QHBoxLayout(m_accGroupBox);
+
+    m_accComboBox = new KComboBox(m_accGroupBox);
+    accLayout->addWidget(m_accComboBox, 1);
+    connect(m_accComboBox, static_cast<void (KComboBox::*)(const QString &)>(&KComboBox::currentIndexChanged), this, &GoogleSettingsDialog::currentAccountChanged);
+
+    m_addAccButton = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add-user")), i18n("&Add"), m_accGroupBox);
+    accLayout->addWidget(m_addAccButton);
+    connect(m_addAccButton, &QPushButton::clicked, this, &GoogleSettingsDialog::slotAddAccountClicked);
+
+    m_removeAccButton = new QPushButton(QIcon::fromTheme(QStringLiteral("list-remove-user")), i18n("&Remove"), m_accGroupBox);
+    accLayout->addWidget(m_removeAccButton);
+    connect(m_removeAccButton, &QPushButton::clicked, this, &GoogleSettingsDialog::slotRemoveAccountClicked);
+
+    QGroupBox *refreshBox = new QGroupBox(i18n("Refresh"), this);
+    mainLayout->addWidget(refreshBox);
+    QGridLayout *refreshLayout = new QGridLayout(refreshBox);
+
+    m_enableRefresh = new QCheckBox(i18n("Enable interval refresh"), refreshBox);
+    m_enableRefresh->setChecked(Settings::self()->enableIntervalCheck());
+    refreshLayout->addWidget(m_enableRefresh, 0, 0, 1, 2);
+
+    QLabel *label = new QLabel(i18n("Refresh interval:"));
+    refreshLayout->addWidget(label, 1, 0);
+    m_refreshSpinBox = new KPluralHandlingSpinBox(this);
+    m_refreshSpinBox->setMaximum(720);
+    m_refreshSpinBox->setMinimum(10);
+    m_refreshSpinBox->setSingleStep(1);
+    m_refreshSpinBox->setValue(30);
+    m_refreshSpinBox->setDisplayIntegerBase(10);
+    m_refreshSpinBox->setSuffix(ki18np(" minute", " minutes"));
+    m_refreshSpinBox->setEnabled(Settings::self()->enableIntervalCheck());
+    refreshLayout->addWidget(m_refreshSpinBox, 1, 1);
+    connect(m_enableRefresh, &QCheckBox::toggled, m_refreshSpinBox, &KPluralHandlingSpinBox::setEnabled);
+
+    if (m_enableRefresh->isEnabled()) {
+        m_refreshSpinBox->setValue(Settings::self()->intervalCheckTime());
+    }
+
+    QMetaObject::invokeMethod(this, "reloadAccounts", Qt::QueuedConnection);
+}
+
+GoogleSettingsDialog::~GoogleSettingsDialog()
+{
+}
+
+QVBoxLayout *GoogleSettingsDialog::mainLayout() const
+{
+    return m_mainLayout;
+}
+
+GoogleAccountManager *GoogleSettingsDialog::accountManager() const
+{
+    return m_accountManager;
+}
+
+KGAPI2::AccountPtr GoogleSettingsDialog::currentAccount() const
+{
+    return m_accountManager->findAccount(m_accComboBox->currentText());
+}
+
+void GoogleSettingsDialog::reloadAccounts()
+{
+    disconnect(m_accComboBox, SIGNAL(currentIndexChanged(QString)),
+               this, SIGNAL(currentAccountChanged(QString)));
+
+    m_accComboBox->clear();
+
+    const AccountsList accounts = m_accountManager->listAccounts();
+    Q_FOREACH (const AccountPtr &account, accounts) {
+        m_accComboBox->addItem(account->accountName());
+    }
+
+    int index = m_accComboBox->findText(m_parentResource->settings()->account(), Qt::MatchExactly);
+    if (index > -1) {
+        m_accComboBox->setCurrentIndex(index);
+    }
+
+    disconnect(m_accComboBox, SIGNAL(currentIndexChanged(QString)),
+               this, SIGNAL(currentAccountChanged(QString)));
+
+    Q_EMIT currentAccountChanged(m_accComboBox->currentText());
+}
+
+void GoogleSettingsDialog::slotAddAccountClicked()
+{
+    AccountPtr account(new Account());
+    // FIXME: We need a proper API for this
+    account->addScope(Account::contactsScopeUrl());
+    account->addScope(Account::calendarScopeUrl());
+    account->addScope(Account::tasksScopeUrl());
+    account->addScope(Account::accountInfoEmailScopeUrl());
+    account->addScope(Account::accountInfoScopeUrl());
+
+    AuthJob *authJob = new AuthJob(account,
+                                   m_parentResource->settings()->clientId(),
+                                   m_parentResource->settings()->clientSecret());
+    connect(authJob, &AuthJob::finished, this, &GoogleSettingsDialog::slotAccountAuthenticated);
+}
+
+void GoogleSettingsDialog::slotRemoveAccountClicked()
+{
+    const AccountPtr account = currentAccount();
+    if (!account) {
+        return;
+    }
+
+    if (KMessageBox::warningYesNo(
+                this,
+                i18n("Do you really want to revoke access to account <b>%1</b>?"
+                     "<p>This will revoke access to all resources using this account!</p>",
+                     account->accountName()),
+                i18n("Revoke Access?"),
+                KStandardGuiItem::yes(),
+                KStandardGuiItem::no(),
+                QString(),
+                KMessageBox::Dangerous) != KMessageBox::Yes) {
+
+        return;
+    }
+
+    m_accountManager->removeAccount(account->accountName());
+    reloadAccounts();
+}
+
+void GoogleSettingsDialog::slotAccountAuthenticated(Job *job)
+{
+    AuthJob *authJob = qobject_cast<AuthJob *>(job);
+    const AccountPtr account = authJob->account();
+
+    if (!m_accountManager->storeAccount(account)) {
+        qWarning() << "Failed to add account to KWallet";
+    }
+
+    reloadAccounts();
+}
+
+bool GoogleSettingsDialog::handleError(Job *job)
+{
+    if ((job->error() == KGAPI2::NoError) || (job->error() == KGAPI2::OK)) {
+        return true;
+    }
+
+    if (job->error() == KGAPI2::Unauthorized) {
+        qDebug() << job << job->errorString();
+        const AccountPtr account = currentAccount();
+        const QList<QUrl> resourceScopes = m_parentResource->scopes();
+        Q_FOREACH (const QUrl &scope, resourceScopes) {
+            if (!account->scopes().contains(scope)) {
+                account->addScope(scope);
+            }
+        }
+
+        AuthJob *authJob = new AuthJob(account, m_parentResource->settings()->clientId(),
+                                       m_parentResource->settings()->clientSecret(), this);
+        authJob->setProperty(JOB_PROPERTY, QVariant::fromValue(job));
+        connect(authJob, &AuthJob::finished, this, &GoogleSettingsDialog::slotAuthJobFinished);
+
+        return false;
+    }
+
+    KMessageBox::sorry(this, job->errorString());
+    job->deleteLater();
+    return false;
+}
+
+void GoogleSettingsDialog::slotAuthJobFinished(Job *job)
+{
+    qDebug();
+
+    if (job->error() != KGAPI2::NoError) {
+        KMessageBox::sorry(this, job->errorString());
+        return;
+    }
+
+    AuthJob *authJob = qobject_cast<AuthJob *>(job);
+    const AccountPtr account = authJob->account();
+    if (!m_accountManager->storeAccount(account)) {
+        qWarning() << "Failed to store account in KWallet";
+    }
+
+    KGAPI2::Job *otherJob = job->property(JOB_PROPERTY).value<KGAPI2::Job *>();
+    otherJob->setAccount(account);
+    otherJob->restart();
+
+    job->deleteLater();
+}
+
+void GoogleSettingsDialog::slotSaveSettings()
+{
+    Settings::self()->setEnableIntervalCheck(m_enableRefresh->isChecked());
+    Settings::self()->setIntervalCheckTime(m_refreshSpinBox->value());
+
+    saveSettings();
+    accept();
+}
+
diff --git a/resources/google/common/googlesettingsdialog.h b/resources/google/common/googlesettingsdialog.h
new file mode 100644 (file)
index 0000000..d2e88c1
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+   Copyright (C) 2013 Daniel Vrátil <dvratil@redhat.com>
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef GOOGLESETTINGSDIALOG_H
+#define GOOGLESETTINGSDIALOG_H
+
+#include <QDialog>
+
+#include <KGAPI/Types>
+
+namespace KGAPI2
+{
+class Job;
+}
+
+class GoogleResource;
+class GoogleSettings;
+class GoogleAccountManager;
+
+class QGroupBox;
+class KComboBox;
+class QCheckBox;
+class KPluralHandlingSpinBox;
+class QPushButton;
+class QVBoxLayout;
+
+class GoogleSettingsDialog : public QDialog
+{
+    Q_OBJECT
+
+public:
+    explicit GoogleSettingsDialog(GoogleAccountManager *accountManager, WId wId, GoogleResource *parent);
+    virtual ~GoogleSettingsDialog();
+
+    GoogleAccountManager *accountManager() const;
+    KGAPI2::AccountPtr currentAccount() const;
+
+public Q_SLOTS:
+    void reloadAccounts();
+
+Q_SIGNALS:
+    void currentAccountChanged(const QString &accountName);
+
+protected:
+    bool handleError(KGAPI2::Job *job);
+    virtual void saveSettings() = 0;
+    QVBoxLayout *mainLayout() const;
+
+private Q_SLOTS:
+    void slotSaveSettings();
+    void slotAddAccountClicked();
+    void slotRemoveAccountClicked();
+    void slotAuthJobFinished(KGAPI2::Job *job);
+    void slotAccountAuthenticated(KGAPI2::Job *job);
+
+private:
+    GoogleResource *m_parentResource;
+    GoogleAccountManager *m_accountManager;
+
+    QGroupBox *m_accGroupBox;
+    QPushButton *m_addAccButton;
+    QPushButton *m_removeAccButton;
+    KComboBox *m_accComboBox;
+    QCheckBox *m_enableRefresh;
+    KPluralHandlingSpinBox *m_refreshSpinBox;
+    QVBoxLayout *m_mainLayout;
+
+};
+
+#endif // GOOGLESETTINGSDIALOG_H
+
diff --git a/resources/google/contacts/CMakeLists.txt b/resources/google/contacts/CMakeLists.txt
new file mode 100644 (file)
index 0000000..9a3571a
--- /dev/null
@@ -0,0 +1,68 @@
+
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_googlecontacts_resource\")
+
+set(contactsresource_SRCS
+  contactsresource.cpp
+  settings.cpp
+  settingsdialog.cpp
+  ../common/googlesettings.cpp
+  ../common/googleresource.cpp
+  ../common/googleaccountmanager.cpp
+  ../common/googlesettingsdialog.cpp
+  ${accounts_SRCS}
+)
+
+kconfig_add_kcfg_files(contactsresource_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/settingsbase.kcfgc)
+
+kcfg_generate_dbus_interface(
+  ${CMAKE_CURRENT_SOURCE_DIR}/settingsbase.kcfg
+  org.kde.Akonadi.GoogleContacts.Settings
+)
+
+qt5_add_dbus_adaptor(contactsresource_SRCS
+  ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.GoogleContacts.Settings.xml
+  ${CMAKE_CURRENT_SOURCE_DIR}/settings.h Settings
+)
+
+add_executable(akonadi_googlecontacts_resource ${contactsresource_SRCS})
+
+if( APPLE )
+  set_target_properties(akonadi_googlecontacts_resource PROPERTIES
+    MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../../Info.plist.template
+  )
+  set_target_properties(akonadi_googlecontacts_resource PROPERTIES
+    MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.googlecontacts"
+  )
+  set_target_properties(akonadi_googlecontacts_resource PROPERTIES
+    MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi Google Contacts Resource"
+  )
+endif()
+
+
+target_link_libraries(akonadi_googlecontacts_resource
+  KF5::AkonadiCore
+  KF5::Contacts
+  KF5::GAPICore
+  KF5::GAPIContacts
+  KF5::AkonadiAgentBase
+  KF5::Wallet
+  KF5::I18n
+  KF5::WindowSystem
+  KF5::Completion
+  KF5::WidgetsAddons
+  KF5::TextWidgets
+  Qt5::DBus
+)
+
+if(${AccountsQt_FOUND} AND ${SignOnQt_FOUND})
+  target_link_libraries(akonadi_googlecontacts_resource
+    ${ACCOUNTSQT_LIBRARIES}
+    ${SIGNONQT_LIBRARIES})
+endif()
+
+install(TARGETS akonadi_googlecontacts_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
+
+install(
+  FILES googlecontactsresource.desktop
+  DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents"
+)
diff --git a/resources/google/contacts/Messages.sh b/resources/google/contacts/Messages.sh
new file mode 100644 (file)
index 0000000..87602e5
--- /dev/null
@@ -0,0 +1,4 @@
+#! /usr/bin/env bash
+$EXTRACTRC *.ui *.kcfg >> rc.cpp
+$XGETTEXT *.cpp -o $podir/akonadi_googlecontacts_resource.pot
+$XGETTEXT ../common/*.cpp -j -o $podir/akonadi_googlecontacts_resource.pot
diff --git a/resources/google/contacts/contactsresource.cpp b/resources/google/contacts/contactsresource.cpp
new file mode 100644 (file)
index 0000000..927b2af
--- /dev/null
@@ -0,0 +1,559 @@
+/*
+    Copyright (C) 2011-2013  Daniel Vrátil <dvratil@redhat.com>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "contactsresource.h"
+#include "settingsdialog.h"
+#include "settings.h"
+
+#include <AkonadiCore/CollectionModifyJob>
+#include <AkonadiCore/EntityDisplayAttribute>
+#include <AkonadiCore/ItemFetchJob>
+#include <AkonadiCore/ItemFetchScope>
+#include <AkonadiCore/ItemModifyJob>
+#include <AkonadiCore/LinkJob>
+#include <AkonadiCore/UnlinkJob>
+#include <AkonadiCore/CachePolicy>
+#include <AkonadiCore/VectorHelper>
+
+#include <KContacts/Addressee>
+#include <KContacts/Picture>
+
+#include <KLocalizedString>
+#include <QDebug>
+#include <QIcon>
+
+#include <QtCore/QPointer>
+
+#include <KGAPI/Types>
+#include <KGAPI/Account>
+#include <KGAPI/AuthJob>
+#include <KGAPI/Contacts/Contact>
+#include <KGAPI/Contacts/ContactCreateJob>
+#include <KGAPI/Contacts/ContactDeleteJob>
+#include <KGAPI/Contacts/ContactFetchJob>
+#include <KGAPI/Contacts/ContactFetchPhotoJob>
+#include <KGAPI/Contacts/ContactModifyJob>
+#include <KGAPI/Contacts/ContactsGroup>
+#include <KGAPI/Contacts/ContactsGroupCreateJob>
+#include <KGAPI/Contacts/ContactsGroupDeleteJob>
+#include <KGAPI/Contacts/ContactsGroupFetchJob>
+#include <KGAPI/Contacts/ContactsGroupModifyJob>
+
+#define MYCONTACTS_REMOTEID QStringLiteral( "MyContacts" )
+#define OTHERCONTACTS_REMOTEID QStringLiteral( "OtherContacts" )
+
+Q_DECLARE_METATYPE(KGAPI2::Job *)
+Q_DECLARE_METATYPE(QList<QString>)
+
+using namespace Akonadi;
+using namespace KGAPI2;
+
+ContactsResource::ContactsResource(const QString &id):
+    GoogleResource(id)
+{
+    updateResourceName();
+}
+
+ContactsResource::~ContactsResource()
+{
+}
+
+GoogleSettings *ContactsResource::settings() const
+{
+    return Settings::self();
+}
+
+int ContactsResource::runConfigurationDialog(WId windowId)
+{
+
+    QScopedPointer<SettingsDialog> settingsDialog(new SettingsDialog(accountManager(), windowId, this));
+    settingsDialog->setWindowIcon(QIcon::fromTheme(QStringLiteral("im-google")));
+
+    return settingsDialog->exec();
+}
+
+void ContactsResource::updateResourceName()
+{
+    const QString accountName = Settings::self()->account();
+    setName(i18nc("%1 is account name (user@gmail.com)", "Google Contacts (%1)", accountName.isEmpty() ? i18n("not configured") : accountName));
+}
+
+QList< QUrl > ContactsResource::scopes() const
+{
+    QList< QUrl > scopes;
+    scopes << Account::contactsScopeUrl()
+           << Account::accountInfoScopeUrl();
+    return scopes;
+}
+
+void ContactsResource::retrieveItems(const Collection &collection)
+{
+    if (!canPerformTask()) {
+        return;
+    }
+
+    // All items are only in top-level collection and Other Contacts collection
+    if ((collection.remoteId() != m_rootCollection.remoteId()) &&
+            (collection.remoteId() != OTHERCONTACTS_REMOTEID)) {
+        itemsRetrievalDone();
+        return;
+    }
+
+    ContactFetchJob *fetchJob = new ContactFetchJob(account(), this);
+    fetchJob->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection));
+    fetchJob->setFetchDeleted(true);
+    if (!collection.remoteRevision().isEmpty()) {
+        fetchJob->setFetchOnlyUpdated(collection.remoteRevision().toLongLong());
+    }
+    connect(fetchJob, &ContactFetchJob::progress, this, &ContactsResource::emitPercent);
+    connect(fetchJob, &ContactFetchJob::finished, this, &ContactsResource::slotItemsRetrieved);
+}
+
+void ContactsResource::retrieveContactsPhotos(const QVariant &arguments)
+{
+    if (!canPerformTask()) {
+        return;
+    }
+
+    const QVariantMap map = arguments.toMap();
+    const Collection collection = map[ QStringLiteral("collection") ].value<Collection>();
+    ItemFetchJob *itemFetchJob = new ItemFetchJob(collection, this);
+    itemFetchJob->setProperty("modifiedItems", map[ QStringLiteral("modifiedItems") ]);
+    itemFetchJob->fetchScope().fetchFullPayload(true);
+    connect(itemFetchJob, &ItemFetchJob::finished, this, &ContactsResource::slotUpdatePhotosItemsRetrieved);
+    Q_EMIT status(Running, i18nc("@info:status", "Retrieving photos"));
+}
+
+void ContactsResource::retrieveCollections()
+{
+    if (!canPerformTask()) {
+        return;
+    }
+
+    ContactsGroupFetchJob *fetchJob = new ContactsGroupFetchJob(account(), this);
+    connect(fetchJob, &ContactFetchJob::progress, this, &ContactsResource::emitPercent);
+    connect(fetchJob, &ContactFetchJob::finished, this, &ContactsResource::slotCollectionsRetrieved);
+}
+
+void ContactsResource::itemAdded(const Item &item, const Collection &collection)
+{
+    if (!canPerformTask<KContacts::Addressee>(item, KContacts::Addressee::mimeType())) {
+        return;
+    }
+
+    KContacts::Addressee addressee = item.payload< KContacts::Addressee >();
+    ContactPtr contact(new Contact(addressee));
+
+    /* If the contact has been moved into My Contacts group then modify the membership */
+    if (collection.remoteId() == MYCONTACTS_REMOTEID) {
+        contact->addGroup(QStringLiteral("http://www.google.com/m8/feeds/groups/%1/base/6").arg(QString::fromLatin1(QUrl::toPercentEncoding(account()->accountName()))));
+    }
+
+    /* If the contact has been moved to Other Contacts then remove all groups */
+    if (collection.remoteId() == OTHERCONTACTS_REMOTEID) {
+        contact->clearGroups();
+    }
+
+    ContactCreateJob *createJob = new ContactCreateJob(contact, account(), this);
+    createJob->setProperty(ITEM_PROPERTY, QVariant::fromValue(item));
+    connect(createJob, &ContactCreateJob::progress, this, &ContactsResource::emitPercent);
+    connect(createJob, &ContactCreateJob::finished, this, &ContactsResource::slotCreateJobFinished);
+}
+
+void ContactsResource::itemChanged(const Item &item, const QSet< QByteArray > &partIdentifiers)
+{
+    Q_UNUSED(partIdentifiers);
+
+    if (!canPerformTask<KContacts::Addressee>(item, KContacts::Addressee::mimeType())) {
+        return;
+    }
+
+    KContacts::Addressee addressee = item.payload< KContacts::Addressee >();
+    ContactPtr contact(new Contact(addressee));
+
+    if (item.parentCollection().remoteId() == MYCONTACTS_REMOTEID) {
+        contact->addGroup(QStringLiteral("http://www.google.com/m8/feeds/groups/%1/base/6").arg(QString::fromLatin1(QUrl::toPercentEncoding(account()->accountName()))));
+    }
+
+    ContactModifyJob *modifyJob = new ContactModifyJob(contact, account(), this);
+    modifyJob->setProperty(ITEM_PROPERTY, QVariant::fromValue(item));
+    connect(modifyJob, &ContactModifyJob::progress, this, &ContactsResource::emitPercent);
+    connect(modifyJob, &ContactModifyJob::finished, this, &ContactsResource::slotGenericJobFinished);
+}
+
+void ContactsResource::itemMoved(const Item &item, const Collection &collectionSource,
+                                 const Collection &collectionDestination)
+{
+    if (!canPerformTask<KContacts::Addressee>(item, KContacts::Addressee::mimeType())) {
+        return;
+    }
+
+    KContacts::Addressee addressee = item.payload< KContacts::Addressee >();
+    ContactPtr contact(new Contact(addressee));
+
+    // MyContacts -> OtherContacts
+    if (collectionSource.remoteId() == MYCONTACTS_REMOTEID &&
+            collectionDestination.remoteId() == OTHERCONTACTS_REMOTEID) {
+        contact->removeGroup(QStringLiteral("http://www.google.com/m8/feeds/groups/%1/base/6").arg(QString::fromLatin1(QUrl::toPercentEncoding(account()->accountName()))));
+
+        // OtherContacts -> MyContacts
+    } else if (collectionSource.remoteId() == OTHERCONTACTS_REMOTEID &&
+               collectionDestination.remoteId() == MYCONTACTS_REMOTEID) {
+        contact->addGroup(QStringLiteral("http://www.google.com/m8/feeds/groups/%1/base/6").arg(QString::fromLatin1(QUrl::toPercentEncoding(account()->accountName()))));
+
+    } else {
+        cancelTask(i18n("Invalid source or destination collection"));
+        return;
+    }
+
+    ContactModifyJob *modifyJob = new ContactModifyJob(contact, account(), this);
+    modifyJob->setProperty(ITEM_PROPERTY, QVariant::fromValue(item));
+    connect(modifyJob, &ContactModifyJob::progress, this, &ContactsResource::emitPercent);
+    connect(modifyJob, &ContactModifyJob::finished, this, &ContactsResource::slotGenericJobFinished);
+}
+
+void ContactsResource::itemRemoved(const Item &item)
+{
+    if (!canPerformTask()) {
+        return;
+    }
+
+    ContactDeleteJob *deleteJob = new ContactDeleteJob(item.remoteId(), account(), this);
+    deleteJob->setProperty(ITEM_PROPERTY, QVariant::fromValue(item));
+    connect(deleteJob, &ContactDeleteJob::progress, this, &ContactsResource::emitPercent);
+    connect(deleteJob, &ContactDeleteJob::finished, this, &ContactsResource::slotGenericJobFinished);
+
+    Q_EMIT status(Running, i18nc("@info:status", "Removing contact"));
+}
+
+void ContactsResource::itemLinked(const Item &item, const Collection &collection)
+{
+    if (!canPerformTask<KContacts::Addressee>(item, KContacts::Addressee::mimeType())) {
+        return;
+    }
+
+    KContacts::Addressee addressee = item.payload<KContacts::Addressee>();
+    ContactPtr contact(new Contact(addressee));
+
+    contact->addGroup(collection.remoteId());
+
+    ContactModifyJob *modifyJob = new ContactModifyJob(contact, account(), this);
+    connect(modifyJob, &ContactModifyJob::finished, this, &ContactsResource::slotGenericJobFinished);
+}
+
+void ContactsResource::itemUnlinked(const Item &item, const Collection &collection)
+{
+    if (!canPerformTask<KContacts::Addressee>(item, KContacts::Addressee::mimeType())) {
+        return;
+    }
+
+    KContacts::Addressee addressee = item.payload<KContacts::Addressee>();
+    ContactPtr contact(new Contact(addressee));
+
+    contact->removeGroup(collection.remoteId());
+
+    ContactModifyJob *modifyJob = new ContactModifyJob(contact, account(), this);
+    modifyJob->setProperty(ITEM_PROPERTY, QVariant::fromValue(item));
+    connect(modifyJob, &ContactModifyJob::finished, this, &ContactsResource::slotGenericJobFinished);
+}
+
+void ContactsResource::collectionAdded(const Akonadi::Collection &collection,
+                                       const Akonadi::Collection &parent)
+{
+    Q_UNUSED(parent);
+
+    if (!canPerformTask()) {
+        return;
+    }
+
+    ContactsGroupPtr group(new ContactsGroup);
+    group->setTitle(collection.name());
+    group->setIsSystemGroup(false);
+
+    ContactsGroupCreateJob *createJob = new ContactsGroupCreateJob(group, account(), this);
+    createJob->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection));
+    connect(createJob, &ContactCreateJob::finished, this, &ContactsResource::slotCreateJobFinished);
+}
+
+void ContactsResource::collectionChanged(const Akonadi::Collection &collection)
+{
+    if (!canPerformTask()) {
+        return;
+    }
+
+    ContactsGroupPtr group(new ContactsGroup());
+    group->setId(collection.remoteId());
+
+    group->setTitle(collection.displayName());
+
+    ContactsGroupModifyJob *modifyJob = new ContactsGroupModifyJob(group, account(), this);
+    modifyJob->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection));
+    connect(modifyJob, &ContactModifyJob::finished, this, &ContactsResource::slotGenericJobFinished);
+}
+
+void ContactsResource::collectionRemoved(const Akonadi::Collection &collection)
+{
+    if (!canPerformTask()) {
+        return;
+    }
+
+    ContactsGroupDeleteJob *deleteJob = new ContactsGroupDeleteJob(collection.remoteId(), account(), this);
+    deleteJob->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection));
+    connect(deleteJob, &ContactDeleteJob::finished, this, &ContactsResource::slotGenericJobFinished);
+}
+
+void ContactsResource::slotCollectionsRetrieved(KGAPI2::Job *job)
+{
+    if (!handleError(job)) {
+        return;
+    }
+
+    ContactsGroupFetchJob *fetchJob = qobject_cast<ContactsGroupFetchJob *>(job);
+    const ObjectsList objects = fetchJob->items();
+
+    CachePolicy cachePolicy;
+    if (Settings::self()->enableIntervalCheck()) {
+        cachePolicy.setInheritFromParent(false);
+        cachePolicy.setIntervalCheckTime(Settings::self()->intervalCheckTime());
+    }
+
+    m_rootCollection = Collection();
+    m_rootCollection.setContentMimeTypes(QStringList() << Collection::virtualMimeType()
+                                         << KContacts::Addressee::mimeType());
+    m_rootCollection.setRemoteId(MYCONTACTS_REMOTEID);
+    m_rootCollection.setName(fetchJob->account()->accountName());
+    m_rootCollection.setParentCollection(Collection::root());
+    m_rootCollection.setCachePolicy(cachePolicy);
+    m_rootCollection.setRights(Collection::CanCreateCollection |
+                               Collection::CanCreateItem |
+                               Collection::CanChangeItem |
+                               Collection::CanDeleteItem);
+
+    EntityDisplayAttribute *attr = m_rootCollection.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
+    attr->setDisplayName(fetchJob->account()->accountName());
+    attr->setIconName(QStringLiteral("im-google"));
+
+    m_collections[ MYCONTACTS_REMOTEID ] = m_rootCollection;
+
+    foreach (const ObjectPtr &object, objects) {
+        const ContactsGroupPtr group = object.dynamicCast<ContactsGroup>();
+
+        QString realName = group->title();
+
+        if (group->isSystemGroup()) {
+            if (group->title().contains(QStringLiteral("Coworkers"))) {
+                realName = i18nc("Name of a group of contacts", "Coworkers");
+            } else if (group->title().contains(QStringLiteral("Friends"))) {
+                realName = i18nc("Name of a group of contacts", "Friends");
+            } else if (group->title().contains(QStringLiteral("Family"))) {
+                realName = i18nc("Name of a group of contacts", "Family");
+            } else if (group->title().contains(QStringLiteral("My Contacts"))) {
+                // Yes, skip My Contacts group, we store "My Contacts" in root collection
+                continue;
+            }
+        } else {
+            if (group->title().contains(QStringLiteral("Other Contacts"))) {
+                realName = i18nc("Name of a group of contacts", "Other Contacts");
+            }
+        }
+
+        Collection collection;
+        collection.setContentMimeTypes(QStringList() << KContacts::Addressee::mimeType());
+        collection.setName(group->id());
+        collection.setParentCollection(m_rootCollection);
+        collection.setRights(Collection::CanLinkItem |
+                             Collection::CanUnlinkItem |
+                             Collection::CanChangeItem);
+        if (!group->isSystemGroup()) {
+            collection.setRights(collection.rights() |
+                                 Collection::CanChangeCollection |
+                                 Collection::CanDeleteCollection);
+        }
+        collection.setRemoteId(group->id());
+        collection.setVirtual(true);
+
+        EntityDisplayAttribute *attr = collection.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
+        attr->setDisplayName(realName);
+        attr->setIconName(QStringLiteral("view-pim-contacts"));
+
+        m_collections[ collection.remoteId() ] = collection;
+    }
+
+    Collection otherCollection;
+    otherCollection.setContentMimeTypes(QStringList() << KContacts::Addressee::mimeType());
+    otherCollection.setName(i18n("Other Contacts"));
+    otherCollection.setParentCollection(m_rootCollection);
+    otherCollection.setRights(Collection::CanCreateItem |
+                              Collection::CanChangeItem |
+                              Collection::CanDeleteItem);
+    otherCollection.setRemoteId(OTHERCONTACTS_REMOTEID);
+
+    attr = otherCollection.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
+    attr->setDisplayName(i18n("Other Contacts"));
+    attr->setIconName(QStringLiteral("view-pim-contacts"));
+    m_collections[ OTHERCONTACTS_REMOTEID ] = otherCollection;
+
+    collectionsRetrieved(Akonadi::valuesToVector(m_collections));
+    job->deleteLater();
+}
+
+void ContactsResource::slotItemsRetrieved(KGAPI2::Job *job)
+{
+    if (!handleError(job)) {
+        return;
+    }
+
+    ContactFetchJob *fetchJob = qobject_cast<ContactFetchJob *>(job);
+    const ObjectsList objects = fetchJob->items();
+
+    Collection collection = fetchJob->property(COLLECTION_PROPERTY).value<Collection>();
+
+    Item::List changedItems, removedItems;
+    QMap<QString, Item::List> groupsMap;
+    QList<QString> changedPhotos;
+    foreach (const ObjectPtr &object, objects) {
+        const ContactPtr contact = object.dynamicCast<Contact>();
+
+        if (((collection.remoteId() == OTHERCONTACTS_REMOTEID) && !contact->groups().isEmpty()) ||
+                ((collection.remoteId() == MYCONTACTS_REMOTEID) && contact->groups().isEmpty())) {
+            continue;
+        }
+
+        Item item;
+        item.setMimeType(KContacts::Addressee::mimeType());
+        item.setParentCollection(m_collections[MYCONTACTS_REMOTEID]);
+        item.setRemoteId(contact->uid());
+        item.setRemoteRevision(contact->etag());
+        item.setPayload<KContacts::Addressee>(*contact.dynamicCast<KContacts::Addressee>());
+
+        if (contact->deleted()) {
+            removedItems << item;
+        } else {
+            changedItems << item;
+            changedPhotos << contact->uid();
+        }
+
+        const QStringList groups = contact->groups();
+        foreach (const QString &group, groups) {
+            groupsMap[group] << item;
+        }
+    }
+
+    itemsRetrievedIncremental(changedItems, removedItems);
+
+    QMap<QString, Item::List>::ConstIterator iter;
+
+    for (iter = groupsMap.constBegin(); iter != groupsMap.constEnd(); ++iter) {
+        new LinkJob(m_collections[iter.key()], iter.value(), this);
+    }
+
+    QVariantMap map;
+    map[QStringLiteral("collection")] = QVariant::fromValue(collection);
+    map[QStringLiteral("modifiedItems")] = QVariant::fromValue(changedPhotos);
+    scheduleCustomTask(this, "retrieveContactsPhotos", map);
+
+    const QDateTime local(QDateTime::currentDateTime());
+    const QDateTime UTC(local.toUTC());
+
+    collection.setRemoteRevision(QString::number(UTC.toTime_t()));
+    new CollectionModifyJob(collection, this);
+
+    job->deleteLater();
+}
+
+void ContactsResource::slotUpdatePhotosItemsRetrieved(KJob *job)
+{
+    ItemFetchJob *fetchJob = qobject_cast<ItemFetchJob *>(job);
+    const Item::List items = fetchJob->items();
+    const QList<QString> modifiedItems = fetchJob->property("modifiedItems").value< QList<QString> >();
+    ContactsList contacts;
+
+    foreach (const Item &item, items) {
+        if (modifiedItems.contains(item.remoteId())) {
+            const KContacts::Addressee addressee = item.payload<KContacts::Addressee>();
+            const ContactPtr contact(new Contact(addressee));
+            contacts << contact;
+        }
+    }
+
+    // Make sure account is still valid
+    if (!canPerformTask()) {
+        return;
+    }
+
+    ContactFetchPhotoJob *photoJob = new ContactFetchPhotoJob(contacts, account(), this);
+    photoJob->setProperty(ITEMLIST_PROPERTY, QVariant::fromValue(items));
+    photoJob->setProperty("processedItems", 0);
+    connect(photoJob, &ContactFetchPhotoJob::photoFetched, this, &ContactsResource::slotUpdatePhotoFinished);
+    connect(photoJob, &ContactFetchPhotoJob::finished, this, &ContactsResource::slotGenericJobFinished);
+}
+
+void ContactsResource::slotUpdatePhotoFinished(KGAPI2::Job *job, const ContactPtr &contact)
+{
+    Item::List items = job->property(ITEMLIST_PROPERTY).value<Item::List>();
+
+    int processedItems = job->property("processedItems").toInt();
+    processedItems++;
+    job->setProperty("processedItems", processedItems);
+    emitPercent(job, processedItems, items.count());
+
+    foreach (Item item, items) {
+        if (item.remoteId() == contact->uid()) {
+            item.setPayload<KContacts::Addressee>(*contact.dynamicCast<KContacts::Addressee>());
+            new ItemModifyJob(item, this);
+            return;
+        }
+    }
+}
+
+void ContactsResource::slotCreateJobFinished(KGAPI2::Job *job)
+{
+    if (!handleError(job)) {
+        return;
+    }
+
+    Item item = job->property(ITEM_PROPERTY).value<Item>();
+    Collection collection = job->property(COLLECTION_PROPERTY).value<Collection>();
+    if (item.isValid()) {
+        ContactCreateJob *createJob = qobject_cast<ContactCreateJob *>(job);
+        Q_ASSERT(createJob->items().count() == 1);
+        ContactPtr contact = createJob->items().at(0).dynamicCast<Contact>();
+
+        item.setRemoteId(contact->uid());
+        item.setRemoteRevision(contact->etag());
+        changeCommitted(item);
+    } else if (collection.isValid()) {
+        ContactsGroupCreateJob *createJob = qobject_cast<ContactsGroupCreateJob *>(job);
+        Q_ASSERT(createJob->items().count() == 1);
+        ContactsGroupPtr group = createJob->items().at(0).dynamicCast<ContactsGroup>();
+
+        collection.setRemoteId(group->id());
+        collection.setContentMimeTypes(QStringList() << KContacts::Addressee::mimeType());
+
+        EntityDisplayAttribute *attr = collection.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
+        attr->setDisplayName(group->title());
+        attr->setIconName(QStringLiteral("view-pim-contacts"));
+
+        m_collections[ collection.remoteId() ] = collection;
+
+        changeCommitted(collection);
+    }
+
+    job->deleteLater();
+}
+
+AKONADI_RESOURCE_MAIN(ContactsResource)
diff --git a/resources/google/contacts/contactsresource.h b/resources/google/contacts/contactsresource.h
new file mode 100644 (file)
index 0000000..25a238b
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+    Copyright (C) 2011, 2012  Dan Vratil <dan@progdan.cz>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef GOOGLE_CONTACTS_CONTACTSRESOURCE_H
+#define GOOGLE_CONTACTS_CONTACTSRESOURCE_H
+
+#include "common/googleresource.h"
+
+#include <AkonadiCore/Collection>
+#include <AkonadiCore/Item>
+
+class GoogleSettings;
+namespace KGAPI2
+{
+class Job;
+}
+class KJob;
+
+class ContactsResource: public GoogleResource
+{
+    Q_OBJECT
+
+public:
+    using GoogleResource::collectionChanged; // So we don't trigger -Woverloaded-virtual
+    explicit ContactsResource(const QString &id);
+
+    ~ContactsResource();
+
+protected Q_SLOTS:
+    void retrieveCollections() Q_DECL_OVERRIDE;
+    void retrieveItems(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    virtual void retrieveContactsPhotos(const QVariant &argument);
+
+    void itemRemoved(const Akonadi::Item &item) Q_DECL_OVERRIDE;
+    void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    void itemChanged(const Akonadi::Item &item, const QSet< QByteArray > &partIdentifiers) Q_DECL_OVERRIDE;
+    void itemMoved(const Akonadi::Item &item, const Akonadi::Collection &collectionSource,
+                   const Akonadi::Collection &collectionDestination) Q_DECL_OVERRIDE;
+
+    void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) Q_DECL_OVERRIDE;
+    void collectionChanged(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    void collectionRemoved(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+
+    void itemLinked(const Akonadi::Item &item, const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    void itemUnlinked(const Akonadi::Item &item, const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+
+    void slotItemsRetrieved(KGAPI2::Job *job);
+    void slotCollectionsRetrieved(KGAPI2::Job *job);
+
+    void slotUpdatePhotosItemsRetrieved(KJob *job);
+    void slotUpdatePhotoFinished(KGAPI2::Job *job, const KGAPI2::ContactPtr &contact);
+
+    void slotCreateJobFinished(KGAPI2::Job *job);
+
+    GoogleSettings *settings() const Q_DECL_OVERRIDE;
+    int runConfigurationDialog(WId windowId) Q_DECL_OVERRIDE;
+    void updateResourceName() Q_DECL_OVERRIDE;
+    QList< QUrl > scopes() const Q_DECL_OVERRIDE;
+
+private:
+
+    QMap<QString, Akonadi::Collection> m_collections;
+    Akonadi::Collection m_rootCollection;
+
+};
+
+#endif // CONTACTSRESOURCE_H
diff --git a/resources/google/contacts/googlecontactsresource.desktop b/resources/google/contacts/googlecontactsresource.desktop
new file mode 100644 (file)
index 0000000..448adb9
--- /dev/null
@@ -0,0 +1,93 @@
+[Desktop Entry]
+Name=Google Contacts
+Name[bg]=Контакти в Google
+Name[bs]=Google kontakti
+Name[ca]=Contactes de Google
+Name[ca@valencia]=Contactes de Google
+Name[cs]=Kontakty Google
+Name[da]=Google-kontakter
+Name[de]=Google-Kontakte
+Name[el]=Google Επαφές
+Name[en_GB]=Google Contacts
+Name[es]=Contactos Google
+Name[et]=Google'i kontaktid
+Name[fi]=Google-yhteystiedot
+Name[fr]=Contacts Google
+Name[ga]=Teagmhálacha Google
+Name[gl]=Google Contacts
+Name[hu]=Google névjegyek
+Name[ia]=Contactos de Google
+Name[it]=Contatti Google
+Name[kk]=Google контакттары
+Name[km]=ទំនាក់ទំនង Google
+Name[ko]=Google 연락처
+Name[lt]=Google kontaktai
+Name[lv]=Google kontakti
+Name[nb]=Google-kontakter
+Name[nds]=Google-Kontakten
+Name[nl]=Google contactpersonen
+Name[pl]=Kontakty Google
+Name[pt]=Contactos do Google
+Name[pt_BR]=Contatos do Google
+Name[ru]=Контакты Google
+Name[sk]=Google kontakty
+Name[sl]=Stiki Google
+Name[sr]=Гуглови контакти
+Name[sr@ijekavian]=Гуглови контакти
+Name[sr@ijekavianlatin]=Googleovi kontakti
+Name[sr@latin]=Googleovi kontakti
+Name[sv]=Google kontakter
+Name[tr]=Google Kişileri
+Name[ug]=Google ئالاقەداشلىرى
+Name[uk]=Контакти Google
+Name[x-test]=xxGoogle Contactsxx
+Name[zh_CN]=Google 联系人
+Name[zh_TW]=Google 聯絡人
+Comment=Access your Google Contacts from KDE
+Comment[bg]=Достъп до контактите ви в Google от KDE
+Comment[bs]=Pristupite svojim Google kontaktima iz KDE
+Comment[ca]=Accediu als contactes de Google des del KDE
+Comment[ca@valencia]=Accediu als contactes de Google des del KDE
+Comment[da]=Tilgå dine Google-kontakter fra KDE
+Comment[de]=Greifen Sie in KDE auf Google-Kontakte zu
+Comment[el]=Αποκτήστε πρόσβαση στις Google επαφές σας από το KDE
+Comment[en_GB]=Access your Google Contacts from KDE
+Comment[es]=Acceda a sus contactos Google desde KDE
+Comment[et]=Oma Google'i kontaktide kasutamine otse KDE-st
+Comment[fi]=Google-yhteystietoihin pääsy KDE:sta
+Comment[fr]=Accès à vos contacts Google depuis KDE
+Comment[gl]=Acceda aos seus contactos de Google desde KDE.
+Comment[hu]=A Google névjegyeinek elérése a KDE-ből
+Comment[ia]=Accede a tu Contactos de Google ab KDE
+Comment[it]=Accedi ai tuoi contatti Google da KDE
+Comment[kk]=Google контакттарына KDE-ден қатынау
+Comment[km]=ចូល​ដំណើរការ​ទំនាក់ទំនង Google របស់​អ្នក​ពី KDE
+Comment[ko]=KDE에서 Google 연락처에 접근하기
+Comment[lt]=Pasiekite savo Google kontaktus iš KDE
+Comment[lv]=Piekļūstiet saviem Google kontaktiem no KDE
+Comment[nb]=Bruk dine Google-kontakter fra KDE
+Comment[nds]=Ut KDE op Dien Google-Kontakten togriepen
+Comment[nl]=Heb toegang tot uw Google contactpersonen vanuit KDE 
+Comment[pl]=Uzyskaj dostęp do Kontaktów Google z KDE
+Comment[pt]=Aceda aos seus contactos da Google a partir do KDE
+Comment[pt_BR]=Acesse seus contatos do Google a partir do KDE
+Comment[ru]=Доступ к контактам Google из KDE
+Comment[sk]=Pristupuje k vašim Google kontaktom z KDE
+Comment[sl]=Dostopajte do svojih stikov Google
+Comment[sr]=Приступите својим контактима на Гуглу из КДЕ‑а
+Comment[sr@ijekavian]=Приступите својим контактима на Гуглу из КДЕ‑а
+Comment[sr@ijekavianlatin]=Pristupite svojim kontaktima na Googleu iz KDE‑a
+Comment[sr@latin]=Pristupite svojim kontaktima na Googleu iz KDE‑a
+Comment[sv]=Kom åt Google kontakter från KDE
+Comment[tr]=Google Kişilerinize KDE'den erişin
+Comment[uk]=Доступ до ваших записів контактів Google з KDE
+Comment[x-test]=xxAccess your Google Contacts from KDExx
+Comment[zh_CN]=在 KDE 中访问您的 Google 联系人
+Comment[zh_TW]=用 KDE 存取您的 Google 聯絡人
+Type=AkonadiResource
+Exec=akonadi_googlecontacts_resource
+X-Akonadi-MimeTypes=text/directory,
+X-Akonadi-Capabilities=Resource
+X-Akonadi-Identifier=akonadi_googlecontacts_resource
+X-Akonadi-Custom-KAccounts=google-contacts,google-calendar
+Icon=im-google
diff --git a/resources/google/contacts/settings.cpp b/resources/google/contacts/settings.cpp
new file mode 100644 (file)
index 0000000..86240d8
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+    Copyright (C) 2011-2013  Daniel Vrátil <dvratil@redhat.com>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "settings.h"
+#include "settingsadaptor.h"
+
+#include <QDBusConnection>
+
+class SettingsHelper
+{
+public:
+    SettingsHelper() : q(Q_NULLPTR)
+    {
+    }
+
+    ~SettingsHelper()
+    {
+        delete q;
+        q = Q_NULLPTR;
+    }
+
+    Settings *q;
+};
+
+Q_GLOBAL_STATIC(SettingsHelper, s_globalSettings)
+
+Settings::Settings():
+    GoogleSettings()
+{
+    Q_ASSERT(!s_globalSettings->q);
+    s_globalSettings->q = this;
+
+    new SettingsAdaptor(this);
+    QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"), this,
+            QDBusConnection::ExportAdaptors | QDBusConnection::ExportScriptableContents);
+}
+
+Settings *Settings::self()
+{
+    if (!s_globalSettings->q) {
+        new Settings;
+        s_globalSettings->q->load();
+    }
+
+    return s_globalSettings->q;
+
+}
diff --git a/resources/google/contacts/settings.h b/resources/google/contacts/settings.h
new file mode 100644 (file)
index 0000000..a7ab14c
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+    Copyright (C) 2011-2012  Daniel Vrátil <dvratil@redhat.com>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef GOOGLE_CONTACTS_SETTINGS_H
+#define GOOGLE_CONTACTS_SETTINGS_H
+
+#include "common/googlesettings.h"
+
+class Settings: public GoogleSettings
+{
+    Q_OBJECT
+    Q_CLASSINFO("D-Bus Interface", "org.kde.Akonadi.GoogleContacts.ExtendedSettings")
+public:
+    Settings();
+    static Settings *self();
+
+};
+
+#endif // SETTINGS_H
diff --git a/resources/google/contacts/settingsbase.kcfg b/resources/google/contacts/settingsbase.kcfg
new file mode 100644 (file)
index 0000000..c66d0b3
--- /dev/null
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:kcfg="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
+                         http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
+  <kcfgfile/>
+  <group name="General">
+    <entry name="Account" type="String">
+    </entry>
+    <!-- AccountName and accountId are used only when using
+         KAccounts instead of native authentication //-->
+    <entry name="AccountName" type="String">
+    </entry>
+    <entry name="AccountId" type="Int">
+        <default>0</default>
+    </entry>
+
+    <entry name="EnableIntervalCheck" type="Bool">
+      <default>false</default>
+    </entry>
+    <entry name="IntervalCheckTime" type="Int">
+      <default>60</default>
+    </entry>
+  </group>
+</kcfg>
diff --git a/resources/google/contacts/settingsbase.kcfgc b/resources/google/contacts/settingsbase.kcfgc
new file mode 100644 (file)
index 0000000..8eda3d3
--- /dev/null
@@ -0,0 +1,7 @@
+File=settingsbase.kcfg
+ClassName=SettingsBase
+Mutators=true
+ItemAccessors=true
+SetUserTexts=true
+Singleton=false
+GlobalEnums=true
\ No newline at end of file
diff --git a/resources/google/contacts/settingsdialog.cpp b/resources/google/contacts/settingsdialog.cpp
new file mode 100644 (file)
index 0000000..3fa8ff4
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+    Copyright (C) 2011-2013  Daniel Vrátil <dvratil@redhat.com>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "settingsdialog.h"
+#include "settings.h"
+
+#include <KWindowSystem>
+#include <KLocalizedString>
+#include <QDebug>
+
+#include <KGAPI/Account>
+#include <KGAPI/AuthJob>
+
+using namespace KGAPI2;
+
+SettingsDialog::SettingsDialog(GoogleAccountManager *accountMgr, WId windowId, GoogleResource *parent):
+    GoogleSettingsDialog(accountMgr, windowId, parent)
+{
+}
+
+SettingsDialog::~SettingsDialog()
+{
+}
+
+void SettingsDialog::saveSettings()
+{
+    const AccountPtr account = currentAccount();
+    if (!account) {
+        Settings::self()->setAccount(QString());
+        Settings::self()->save();
+        return;
+    }
+
+    Settings::self()->setAccount(account->accountName());
+    Settings::self()->save();
+}
diff --git a/resources/google/contacts/settingsdialog.h b/resources/google/contacts/settingsdialog.h
new file mode 100644 (file)
index 0000000..c7fbdd8
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+    Copyright (C) 2011-2013  Daniel Vrátil <dvratil@redhat.com>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef GOOGLE_CONTACTS_SETTINGSDIALOG_H
+#define GOOGLE_CONTACTS_SETTINGSDIALOG_H
+
+#include "common/googlesettingsdialog.h"
+
+class GoogleAccountManager;
+
+class SettingsDialog : public GoogleSettingsDialog
+{
+    Q_OBJECT
+public:
+    explicit SettingsDialog(GoogleAccountManager *accountMgr, WId windowId, GoogleResource *parent);
+    ~SettingsDialog();
+
+private Q_SLOTS:
+    void saveSettings() Q_DECL_OVERRIDE;
+
+};
+
+#endif // SETTINGSDIALOG_H
diff --git a/resources/ical/CMakeLists.txt b/resources/ical/CMakeLists.txt
new file mode 100644 (file)
index 0000000..b77f4ae
--- /dev/null
@@ -0,0 +1,45 @@
+include_directories(
+    ${kdepim-runtime_SOURCE_DIR}
+    ${CMAKE_CURRENT_BINARY_DIR}
+    ${CMAKE_CURRENT_SOURCE_DIR}/shared
+)
+
+
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_ical_resource\")
+
+
+add_subdirectory( wizard )
+add_subdirectory( notes )
+
+if(BUILD_TESTING)
+    add_subdirectory( autotests )
+endif()
+
+########### next target ###############
+add_definitions( -DSETTINGS_NAMESPACE=Akonadi_ICal_Resource )
+
+set( icalresource_SRCS
+  icalresource.cpp
+  shared/icalresourcebase.cpp
+  shared/icalresource.cpp
+)
+
+install( FILES icalresource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents" )
+
+kconfig_add_kcfg_files(icalresource_SRCS settings.kcfgc)
+kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/icalresource.kcfg org.kde.Akonadi.ICal.Settings)
+qt5_add_dbus_adaptor(icalresource_SRCS
+  ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.ICal.Settings.xml settings.h Akonadi_ICal_Resource::Settings icalsettingsadaptor ICalSettingsAdaptor
+)
+
+add_executable(akonadi_ical_resource ${icalresource_SRCS})
+
+if( APPLE )
+  set_target_properties(akonadi_ical_resource PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../Info.plist.template)
+  set_target_properties(akonadi_ical_resource PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.ICal")
+  set_target_properties(akonadi_ical_resource PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi ICal Resource")
+endif ()
+
+target_link_libraries(akonadi_ical_resource KF5::AkonadiCore   KF5::KIOCore KF5::CalendarCore KF5::AkonadiAgentBase akonadi-singlefileresource KF5::DBusAddons)
+
+install(TARGETS akonadi_ical_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
diff --git a/resources/ical/Messages.sh b/resources/ical/Messages.sh
new file mode 100644 (file)
index 0000000..6361b6f
--- /dev/null
@@ -0,0 +1,4 @@
+#! /usr/bin/env bash
+$EXTRACTRC `find . -name \*.kcfg -o -name \*.ui` >> rc.cpp || exit 11
+$XGETTEXT `find . -name \*.cpp -o -name \*.h` -o $podir/akonadi_ical_resource.pot
+rm -f rc.cpp
diff --git a/resources/ical/autotests/CMakeLists.txt b/resources/ical/autotests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..1a338ca
--- /dev/null
@@ -0,0 +1,4 @@
+configure_file(${CMAKE_CURRENT_SOURCE_DIR}/ical-empty.xml ${CMAKE_CURRENT_BINARY_DIR}/ical-empty.xml COPYONLY)
+configure_file(${CMAKE_CURRENT_SOURCE_DIR}/ical-step1.xml ${CMAKE_CURRENT_BINARY_DIR}/ical-step1.xml COPYONLY)
+
+akonadi_add_resourcetest( ical icaltest.es )
diff --git a/resources/ical/autotests/event.ical b/resources/ical/autotests/event.ical
new file mode 100644 (file)
index 0000000..a7844ee
--- /dev/null
@@ -0,0 +1,26 @@
+BEGIN:VCALENDAR
+PRODID:-//K Desktop Environment//NONSGML libkcal 3.5//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20070109T100625Z
+ORGANIZER;CN=Volker Krause:MAILTO:vkrause@kde.org
+CREATED:20070109T100553Z
+UID:libkcal-1135684253.945
+SEQUENCE:1
+LAST-MODIFIED:20070109T100625Z
+SUMMARY:Test event
+LOCATION:here
+CLASS:PUBLIC
+PRIORITY:5
+CATEGORIES:KDE
+DTSTART:20070109T183000Z
+DTEND:20070109T225900Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+DESCRIPTION:
+ACTION:DISPLAY
+TRIGGER;VALUE=DURATION:-PT45M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+
diff --git a/resources/ical/autotests/ical-empty.xml b/resources/ical/autotests/ical-empty.xml
new file mode 100644 (file)
index 0000000..a1e2d7b
--- /dev/null
@@ -0,0 +1,6 @@
+<knut>
+  <collection content="text/calendar,application/x-vnd.akonadi.calendar.event,application/x-vnd.akonadi.calendar.todo,application/x-vnd.akonadi.calendar.journal,application/x-vnd.akonadi.calendar.freebusy" rid="newical.ics" name="akonadi_ical_resource_97" >
+    <attribute type="ENTITYDISPLAY" >("newical.ics" "office-calendar")</attribute>
+    <attribute type="AccessRights" >wcdW</attribute>
+  </collection>
+</knut>
diff --git a/resources/ical/autotests/ical-step1.xml b/resources/ical/autotests/ical-step1.xml
new file mode 100644 (file)
index 0000000..1e7b9b8
--- /dev/null
@@ -0,0 +1,52 @@
+<knut>
+  <collection content="text/calendar,application/x-vnd.akonadi.calendar.event,application/x-vnd.akonadi.calendar.todo,application/x-vnd.akonadi.calendar.journal,application/x-vnd.akonadi.calendar.freebusy" rid="newical.ics" name="akonadi_ical_resource_1" >
+    <attribute type="ENTITYDISPLAY" >("newical.ics" "office-calendar")</attribute>
+    <attribute type="AccessRights" >wcdW</attribute>
+    <item mimetype="application/x-vnd.akonadi.calendar.event" rid="libkcal-1135684253.945" >
+      <payload>BEGIN:VCALENDAR
+PRODID:-//K Desktop Environment//NONSGML libkcal 3.2//EN
+VERSION:2.0
+BEGIN:VEVENT&#xd;
+DTSTAMP:20090412T123141Z&#xd;
+ORGANIZER;CN="Volker Krause":MAILTO:vkrause@kde.org&#xd;
+CREATED:20070109T100553Z&#xd;
+UID:libkcal-1135684253.945&#xd;
+SEQUENCE:1&#xd;
+LAST-MODIFIED:20070109T100625Z&#xd;
+SUMMARY:Test event&#xd;
+LOCATION:here&#xd;
+PRIORITY:5&#xd;
+CATEGORIES:KDE&#xd;
+DTSTART:20070109T183000Z&#xd;
+DTEND:20070109T225900Z&#xd;
+TRANSP:OPAQUE&#xd;
+BEGIN:VALARM&#xd;
+DESCRIPTION:&#xd;
+ACTION:DISPLAY&#xd;
+TRIGGER;VALUE=DURATION:-PT45M&#xd;
+END:VALARM&#xd;
+END:VEVENT&#xd;
+
+END:VCALENDAR</payload>
+    </item>
+    <item mimetype="application/x-vnd.akonadi.calendar.todo" rid="libkcal-1506191911.958" >
+      <payload>BEGIN:VCALENDAR
+PRODID:-//K Desktop Environment//NONSGML libkcal 3.2//EN
+VERSION:2.0
+BEGIN:VTODO&#xd;
+DTSTAMP:20090412T123141Z&#xd;
+ORGANIZER:MAILTO:vkrause@kde.org&#xd;
+CREATED:20040505T094143Z&#xd;
+UID:libkcal-1506191911.958&#xd;
+LAST-MODIFIED:20040512T133925Z&#xd;
+SUMMARY:Add a demo task to this file&#xd;
+PRIORITY:3&#xd;
+DUE;VALUE=DATE:20090101&#xd;
+COMPLETED:20090101T133925Z&#xd;
+PERCENT-COMPLETE:100&#xd;
+END:VTODO&#xd;
+
+END:VCALENDAR</payload>
+    </item>
+  </collection>
+</knut>
diff --git a/resources/ical/autotests/icaltest.es b/resources/ical/autotests/icaltest.es
new file mode 100644 (file)
index 0000000..06c2687
--- /dev/null
@@ -0,0 +1,31 @@
+Resource.setType( "akonadi_ical_resource" );
+Resource.setPathOption( "Path", "newical.ics" );
+Resource.create();
+
+XmlOperations.setXmlFile( "ical-empty.xml" );
+XmlOperations.setRootCollections( Resource.identifier() );
+XmlOperations.setCollectionKey( "None" );
+XmlOperations.ignoreCollectionField( "Name" ); // name is the resource identifier and thus unpredictable
+XmlOperations.setNormalizeRemoteIds( true );
+XmlOperations.assertEqual();
+
+// item creation
+var i1 = ItemTest.newInstance();
+i1.setParentCollection( Resource.identifier() );
+i1.setMimeType( "text/calendar" );
+i1.setPayloadFromFile( "event.ical" );
+i1.create();
+
+var i2 = ItemTest.newInstance();
+i2.setParentCollection( Resource.identifier() );
+i2.setMimeType( "text/calendar" );
+i2.setPayloadFromFile( "task.ical" );
+i2.create();
+
+Resource.recreate();
+
+XmlOperations.setXmlFile( "ical-step1.xml" );
+XmlOperations.setRootCollections( Resource.identifier() );
+XmlOperations.ignoreCollectionField( "None" );
+XmlOperations.assertEqual();
+
diff --git a/resources/ical/autotests/task.ical b/resources/ical/autotests/task.ical
new file mode 100644 (file)
index 0000000..cc51d6b
--- /dev/null
@@ -0,0 +1,16 @@
+BEGIN:VCALENDAR
+PRODID:-//K Desktop Environment//NONSGML libkcal 3.5//EN
+VERSION:2.0
+BEGIN:VTODO
+DTSTAMP:20090101T154017Z
+ORGANIZER:MAILTO:vkrause@kde.org
+CREATED:20040505T094143Z
+UID:libkcal-1506191911.958
+LAST-MODIFIED:20040512T133925Z
+SUMMARY:Add a demo task to this file
+PRIORITY:3
+DUE;VALUE=DATE:20090101
+COMPLETED:20090101T133925Z
+PERCENT-COMPLETE:100
+END:VTODO
+END:VCALENDAR
diff --git a/resources/ical/icalresource.cpp b/resources/ical/icalresource.cpp
new file mode 100644 (file)
index 0000000..5049c51
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+    Copyright (c) 2015 Daniel Vrátil <dvratil@redhat.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "shared/icalresource.h"
+
+AKONADI_RESOURCE_MAIN(ICalResource)
diff --git a/resources/ical/icalresource.desktop b/resources/ical/icalresource.desktop
new file mode 100644 (file)
index 0000000..493df3a
--- /dev/null
@@ -0,0 +1,104 @@
+[Desktop Entry]
+Name=ICal Calendar File
+Name[ar]=ملف تقويم ICal
+Name[bg]=Календарен файл iCal
+Name[bs]=ICal kalendar datoteka
+Name[ca]=Fitxer de calendari ICal
+Name[ca@valencia]=Fitxer de calendari ICal
+Name[cs]=Soubor s kalendářem iCal
+Name[da]=iCal-kalenderfil
+Name[de]=ICal-Kalenderdatei
+Name[el]=Αρχείο ημερολογίου ICal
+Name[en_GB]=ICal Calendar File
+Name[es]=Archivo de calendario ICal
+Name[et]=ICali kalendrifail
+Name[fi]=iCal-kalenteritiedosto
+Name[fr]=Fichier d'agenda au format « ICal »
+Name[ga]=Comhad Féilire ICal
+Name[gl]=Ficheiro de Calendario de ICal
+Name[hu]=iCal-naptárfájl
+Name[ia]=File iCal de calendario
+Name[it]=Calendario iCal
+Name[ja]=iCal カレンダーファイル
+Name[kk]=ICal күнтізбе файлы
+Name[km]=ឯកសារ​ប្រតិទិន ICal
+Name[ko]=ICal 달력 파일
+Name[lt]=ICal kalendoriaus failas
+Name[lv]=ICal kalendāra fails
+Name[nb]=ICal kalenderfil
+Name[nds]=ICal-Kalennerdatei
+Name[nl]=ICal-agendabestand
+Name[nn]=iCal-kalenderfil
+Name[pa]=ICal ਕੈਲੰਡਰ ਫਾਇਲ
+Name[pl]=Plik kalendarza ICal
+Name[pt]=Ficheiro de Calendário ICal
+Name[pt_BR]=Arquivo de calendário ICal
+Name[ro]=Fișier-calendar ICal
+Name[ru]=Файл календаря iCal
+Name[sk]=Súbor kalendára iCal
+Name[sl]=Koledarska datoteka iCal
+Name[sr]=И‑календарски фајл
+Name[sr@ijekavian]=И‑календарски фајл
+Name[sr@ijekavianlatin]=I‑kalendarski fajl
+Name[sr@latin]=I‑kalendarski fajl
+Name[sv]=ICal-kalenderfil
+Name[tr]=ICal Takvim Dosyası
+Name[uk]=Файл календаря ICal
+Name[x-test]=xxICal Calendar Filexx
+Name[zh_CN]=ICal 日历文件
+Name[zh_TW]=ICal 行事曆檔案
+Comment=Loads data from an iCal file
+Comment[ar]=تحميل البيانات من ملف ICal
+Comment[bg]=Зареждане на данни от файл iCal
+Comment[bs]=Učitava podatke iz iCal datoteke
+Comment[ca]=Carrega les dades des d'un fitxer iCal
+Comment[ca@valencia]=Carrega les dades des d'un fitxer iCal
+Comment[cs]=Načítá data z iCal souboru
+Comment[da]=Indlæser data fra en iCal-fil
+Comment[de]=Daten werden aus einer lokalen iCal-Datei geladen
+Comment[el]=Φόρτωση δεδομένων από ένα αρχείο iCal
+Comment[en_GB]=Loads data from an iCal file
+Comment[es]=Carga datos de un archivo iCal local
+Comment[et]=Andmete laadimine iCal-failist
+Comment[fi]=Noutaa tietoa iCal-tiedostosta
+Comment[fr]=Charge des données depuis un fichier au format « iCal »
+Comment[ga]=Breiseán a luchtaíonn sonraí ó chomhad iCal
+Comment[gl]=Carga datos desde un ficheiro iCal
+Comment[hu]=Adatbetöltés iCal formátumú naptárból
+Comment[ia]=Lege datos de un file iCal
+Comment[it]=Carica dati da un file iCal
+Comment[ja]=iCal ファイルからデータを読み込みます
+Comment[kk]=ICal күнтізбе файлынан деректі алып беру
+Comment[km]=ផ្ទុក​ទិន្នន័យ​ពី​ឯកសារ iCal
+Comment[ko]=로컬 iCal 파일에서 데이터를 가져오기
+Comment[lt]=Įkelia duomenis iš iCal failo
+Comment[lv]=Ielādē datus no iCal faila
+Comment[nb]=Laster data fra en iCal-fil
+Comment[nds]=Laadt Daten ut en ICal-Datei
+Comment[nl]=Laadt gegevens van een iCal-bestand
+Comment[nn]=Lastar data frå ei iCal-fil
+Comment[pa]=iCal ਫਾਇਲ ਤੋਂ ਡਾਟਾ ਡਾਊਨਲੋਡ ਕਰੋ
+Comment[pl]=Wczytuje dane z pliku ICal
+Comment[pt]=Carrega os dados de um ficheiro iCal
+Comment[pt_BR]=Carrega os dados de um arquivo iCal
+Comment[ro]=Încarcă date dintr-un fișier iCal
+Comment[ru]=Загрузка данных из файла календаря iCal
+Comment[sk]=Načíta dáta zo súboru iCal
+Comment[sl]=Naloži podatke iz datoteke iCal
+Comment[sr]=Учитава податке из и‑календарског фајла
+Comment[sr@ijekavian]=Учитава податке из и‑календарског фајла
+Comment[sr@ijekavianlatin]=Učitava podatke iz i‑kalendarskog fajla
+Comment[sr@latin]=Učitava podatke iz i‑kalendarskog fajla
+Comment[sv]=Laddar data från en iCal-fil
+Comment[tr]=iCal dosyasından veri yükler
+Comment[uk]=Завантажує дані з файла iCal
+Comment[x-test]=xxLoads data from an iCal filexx
+Comment[zh_CN]=从 iCal 文件载入数据
+Comment[zh_TW]=從 iCal 檔載入資料
+Type=AkonadiResource
+Exec=akonadi_ical_resource
+Icon=text-calendar
+
+X-Akonadi-MimeTypes=text/calendar,application/x-vnd.akonadi.calendar.event,application/x-vnd.akonadi.calendar.todo,application/x-vnd.akonadi.calendar.journal,application/x-vnd.akonadi.calendar.freebusy
+X-Akonadi-Capabilities=Resource
+X-Akonadi-Identifier=akonadi_ical_resource
diff --git a/resources/ical/icalresource.kcfg b/resources/ical/icalresource.kcfg
new file mode 100644 (file)
index 0000000..972d348
--- /dev/null
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:kcfg="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
+      http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
+  <kcfgfile arg="true" />
+  <group name="General">
+    <entry name="Path" type="Path">
+      <label>Path to iCal file.</label>
+      <default></default>
+    </entry>
+    <entry name="DisplayName" type="String">
+      <label>Display name.</label>
+      <default></default>
+    </entry>
+    <entry name="ReadOnly" type="Bool">
+      <label>Do not change the actual backend data.</label>
+      <default>false</default>
+    </entry>
+    <entry name="MonitorFile" type="Bool">
+      <label>Monitor file for changes.</label>
+      <default>true</default>
+    </entry>
+  </group>
+</kcfg>
diff --git a/resources/ical/notes/CMakeLists.txt b/resources/ical/notes/CMakeLists.txt
new file mode 100644 (file)
index 0000000..c70f491
--- /dev/null
@@ -0,0 +1,31 @@
+include_directories(
+    ${CMAKE_CURRENT_SOURCE_DIR}/../shared
+    ${kdepim-runtime_SOURCE_DIR}
+)
+
+
+
+
+########### next target ###############
+
+add_definitions( -DSETTINGS_NAMESPACE=Akonadi_Aknotes_Resource )
+
+set( notesresource_SRCS
+  ../shared/icalresourcebase.cpp
+  ../shared/icalresource.cpp
+  notesresource.cpp
+)
+
+install( FILES notesresource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents" )
+
+kconfig_add_kcfg_files(notesresource_SRCS settings.kcfgc)
+kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/notesresource.kcfg org.kde.Akonadi.Notes.Settings)
+qt5_add_dbus_adaptor(notesresource_SRCS
+  ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.Notes.Settings.xml settings.h Akonadi_Aknotes_Resource::Settings icalsettingsadaptor ICalSettingsAdaptor
+)
+
+add_executable(akonadi_notes_resource ${notesresource_SRCS})
+
+target_link_libraries(akonadi_notes_resource KF5::AkonadiCore  KF5::KIOCore KF5::CalendarCore KF5::AkonadiAgentBase KF5::DBusAddons akonadi-singlefileresource)
+
+install(TARGETS akonadi_notes_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
diff --git a/resources/ical/notes/notesresource.cpp b/resources/ical/notes/notesresource.cpp
new file mode 100644 (file)
index 0000000..17525ee
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+    Copyright (c) 2009 David Jarvie <djarvie@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "notesresource.h"
+
+#include <KCalCore/Incidence>
+
+#include <kconfigskeleton.h>
+#include <QStandardPaths>
+
+using namespace Akonadi;
+using namespace KCalCore;
+
+static const QLatin1String sNotesType("application/x-vnd.kde.notes");
+
+NotesResource::NotesResource(const QString &id)
+    : ICalResource(id, allMimeTypes(), QStringLiteral("knotes"))
+{
+    KConfigSkeleton::ItemPath *item = static_cast<KConfigSkeleton::ItemPath *>(mSettings->findItem(QStringLiteral("Path")));
+    if (item) {
+        item->setDefaultValue(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + QLatin1String("knotes/"));
+    }
+}
+
+NotesResource::~NotesResource()
+{
+}
+
+QStringList NotesResource::allMimeTypes() const
+{
+    return QStringList() << sNotesType;
+}
+
+QString NotesResource::mimeType(const KCalCore::IncidenceBase::Ptr &) const
+{
+    return sNotesType;
+}
+
+AKONADI_RESOURCE_MAIN(NotesResource)
diff --git a/resources/ical/notes/notesresource.desktop b/resources/ical/notes/notesresource.desktop
new file mode 100644 (file)
index 0000000..a86e3a4
--- /dev/null
@@ -0,0 +1,128 @@
+[Desktop Entry]
+Name=Notes
+Name[af]=Notas
+Name[ar]=ملاحظات
+Name[ast]=Notes
+Name[be]=Заметкі
+Name[bg]=Бележки
+Name[br]=Notennoù
+Name[bs]=Bilješke
+Name[ca]=Notes
+Name[ca@valencia]=Notes
+Name[cs]=Poznámky
+Name[cy]=Nodiadau
+Name[da]=Noter
+Name[de]=Notizen
+Name[el]=Σημειώσεις
+Name[en_GB]=Notes
+Name[eo]=Notoj
+Name[es]=Notas
+Name[et]=Sedelid
+Name[eu]=Oharrak
+Name[fa]=یادداشتها
+Name[fi]=Muistiinpanot
+Name[fr]=Notes
+Name[fy]=Notysjes
+Name[ga]=Nótaí
+Name[gl]=Notas
+Name[he]=פתקים
+Name[hu]=Feljegyzések
+Name[ia]=Notas
+Name[is]=Minnismiðar
+Name[it]=Note
+Name[ja]=メモ
+Name[ka]=ჩანიშვნები
+Name[kk]=Жазбалар
+Name[km]=ចំណាំ
+Name[ko]=노트
+Name[lt]=Užrašai
+Name[lv]=Piezīmes
+Name[mai]=टिप्पणी
+Name[mk]=Белешки
+Name[ms]=Nota
+Name[nb]=Notater
+Name[nds]=Notizen
+Name[ne]=टिपोट
+Name[nl]=Notities
+Name[nn]=Notat
+Name[pa]=ਨੋਟਿਸ
+Name[pl]=Notatki
+Name[pt]=Notas
+Name[pt_BR]=Notas
+Name[ro]=Notițe
+Name[ru]=Заметки
+Name[se]=Nohtat
+Name[sk]=Poznámky
+Name[sl]=Sporočilca
+Name[sq]=Shënimet
+Name[sr]=Белешке
+Name[sr@ijekavian]=Биљешке
+Name[sr@ijekavianlatin]=Bilješke
+Name[sr@latin]=Beleške
+Name[sv]=Anteckningar
+Name[ta]=குறிப்புகள்
+Name[tg]=Ахборот
+Name[th]=บันทึกย่อ
+Name[tr]=Notlar
+Name[ug]=ئىزاھ
+Name[uk]=Примітки
+Name[uz]=Yozma xotira
+Name[uz@cyrillic]=Ёзма хотира
+Name[wa]=Notes
+Name[x-test]=xxNotesxx
+Name[zh_CN]=便笺
+Name[zh_TW]=備忘錄
+Comment=Loads data from a notes file
+Comment[ar]=تحميل البيانات من ملف الملاحظات
+Comment[bg]=Зареждане на данни от файл с бележки
+Comment[bs]=Učitava podatke iz datoteka bilješki
+Comment[ca]=Carrega les dades des d'un fitxer de notes
+Comment[ca@valencia]=Carrega les dades des d'un fitxer de notes
+Comment[da]=Indlæser data fra en notes-fil
+Comment[de]=Lädt Daten aus einer Notizen-Datei
+Comment[el]=Φόρτωση δεδομένων από ένα αρχείο σημειώσεων
+Comment[en_GB]=Loads data from a notes file
+Comment[es]=Carga datos desde un archivo de notas
+Comment[et]=Andmete laadimine sedelite failist
+Comment[fi]=Noutaa tietoa muistiinpanotiedostosta
+Comment[fr]=Charge des données depuis un fichier de notes
+Comment[ga]=Breiseán a luchtaíonn sonraí ó chomhad nótaí
+Comment[gl]=Carga datos desde un ficheiro de notas
+Comment[hu]=Adatbetöltés feljegyzésfájlból
+Comment[ia]=Lege datos de un file de notas
+Comment[it]=Carica dati da un file contenente note
+Comment[ja]=メモファイルからデータを読み込みます
+Comment[kk]=Жазба файлынан деректі алып берді
+Comment[km]=ផ្ទុក​ទិន្នន័យ​ពី​ឯកសារ​ចំណាំ
+Comment[ko]=로컬 노트 파일에서 데이터를 가져오기
+Comment[lt]=Įkelia duomenis iš lipnių lapelių failų
+Comment[lv]=Ielādē datus no piezīmju faila
+Comment[nb]=Laster data fra en notat-fil
+Comment[nds]=Laadt Daten ut en Notizen-Datei
+Comment[nl]=Laadt gegevens van een notitiesbestand
+Comment[nn]=Lastar data frå ei notatfil
+Comment[pa]=ਨੋਟਿਸ ਫਾਇਲ ਤੋਂ ਡਾਟਾ ਡਾਊਨਲੋਡ ਕਰੋ
+Comment[pl]=Wczytuje dane z pliku notatek
+Comment[pt]=Carrega os dados de um ficheiro de notas
+Comment[pt_BR]=Carrega os dados de um arquivo de notas
+Comment[ro]=Încarcă date dintr-un fișier cu note
+Comment[ru]=Загрузка данных из файла заметок
+Comment[sk]=Načíta dáta zo súboru poznámok
+Comment[sl]=Naloži podatke iz datoteke s sporočilci
+Comment[sr]=Учитава податке из фајла белешки
+Comment[sr@ijekavian]=Учитава податке из фајла биљешки
+Comment[sr@ijekavianlatin]=Učitava podatke iz fajla bilješki
+Comment[sr@latin]=Učitava podatke iz fajla beleški
+Comment[sv]=Laddar data från en anteckningsfil
+Comment[tr]=Bir not dosyasından veri yükler
+Comment[uk]=Завантажує дані з файла нотаток
+Comment[x-test]=xxLoads data from a notes filexx
+Comment[zh_CN]=从便笺文件载入数据
+Comment[zh_TW]=從 notes 檔載入資料
+Type=AkonadiResource
+Exec=akonadi_notes_resource
+Icon=view-pim-notes
+
+X-Akonadi-MimeTypes=application/x-vnd.kde.notes
+X-Akonadi-Capabilities=Resource
+X-Akonadi-Identifier=akonadi_notes_resource
diff --git a/resources/ical/notes/notesresource.h b/resources/ical/notes/notesresource.h
new file mode 100644 (file)
index 0000000..b16d348
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+    Copyright (c) 2009 David Jarvie <djarvie@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef NOTESRESOURCE_H
+#define NOTESRESOURCE_H
+
+#include "icalresource.h"
+
+class NotesResource : public ICalResource
+{
+    Q_OBJECT
+
+public:
+    explicit NotesResource(const QString &id);
+    ~NotesResource();
+
+protected:
+    /**
+      Returns the Akonadi specific @c text/calendar sub MIME type of the given @p incidence.
+    */
+    QString mimeType(const KCalCore::IncidenceBase::Ptr &incidence) const Q_DECL_OVERRIDE;
+
+    /**
+      Returns a list of all calendar component sub MIME types.
+     */
+    QStringList allMimeTypes() const Q_DECL_OVERRIDE;
+};
+
+#endif
diff --git a/resources/ical/notes/notesresource.kcfg b/resources/ical/notes/notesresource.kcfg
new file mode 100644 (file)
index 0000000..1ed20ec
--- /dev/null
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:kcfg="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
+      http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
+  <kcfgfile arg="true" />
+  <group name="General">
+    <entry name="Path" type="Path">
+      <label>Path to iCal file.</label>
+      <default></default>
+    </entry>
+    <entry name="DisplayName" type="String">
+      <label>Display name.</label>
+      <default></default>
+    </entry>
+    <entry name="ReadOnly" type="Bool">
+      <label>Do not change the actual backend data.</label>
+      <default>false</default>
+    </entry>
+    <entry name="AutosaveInterval" type="UInt">
+      <label>Autosave interval time (in minutes).</label>
+      <default>1</default>
+    </entry>
+    <entry name="MonitorFile" type="Bool">
+      <label>Monitor file for changes.</label>
+      <default>true</default>
+    </entry>
+  </group>
+</kcfg>
diff --git a/resources/ical/notes/settings.kcfgc b/resources/ical/notes/settings.kcfgc
new file mode 100644 (file)
index 0000000..b305df4
--- /dev/null
@@ -0,0 +1,9 @@
+File=notesresource.kcfg
+ClassName=Settings
+Mutators=true
+ItemAccessors=true
+SetUserTexts=true
+Singleton=false
+#IncludeFiles=
+GlobalEnums=true
+NameSpace=Akonadi_Aknotes_Resource
diff --git a/resources/ical/settings.kcfgc b/resources/ical/settings.kcfgc
new file mode 100644 (file)
index 0000000..829174d
--- /dev/null
@@ -0,0 +1,10 @@
+File=icalresource.kcfg
+ClassName=Settings
+Mutators=true
+ItemAccessors=true
+SetUserTexts=true
+Singleton=false
+#IncludeFiles=
+GlobalEnums=true
+NameSpace=Akonadi_ICal_Resource
+
diff --git a/resources/ical/shared/icalresource.cpp b/resources/ical/shared/icalresource.cpp
new file mode 100644 (file)
index 0000000..327354b
--- /dev/null
@@ -0,0 +1,154 @@
+/*
+    Copyright (c) 2006 Till Adam <adam@kde.org>
+    Copyright (c) 2009 David Jarvie <djarvie@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "icalresource.h"
+
+#include <KCalCore/MemoryCalendar>
+#include <KCalCore/FreeBusy>
+
+#include <QDebug>
+#include <KLocalizedString>
+
+using namespace Akonadi;
+using namespace KCalCore;
+
+ICalResource::ICalResource(const QString &id)
+    : ICalResourceBase(id)
+{
+    QStringList mimeTypes;
+    mimeTypes << QStringLiteral("text/calendar");
+    mimeTypes += allMimeTypes();
+    initialise(mimeTypes, QStringLiteral("office-calendar"));
+}
+
+ICalResource::ICalResource(const QString &id, const QStringList &mimeTypes, const QString &icon)
+    : ICalResourceBase(id)
+{
+    initialise(mimeTypes, icon);
+}
+
+ICalResource::~ICalResource()
+{
+}
+
+bool ICalResource::doRetrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts)
+{
+    Q_UNUSED(parts);
+    const QString rid = item.remoteId();
+    Incidence::Ptr incidence = calendar()->instance(rid);
+    if (!incidence) {
+        qCritical() << "akonadi_ical_resource: Can't find incidence with uid "
+                    << rid << "; item.id() = " << item.id();
+        Q_EMIT error(i18n("Incidence with uid '%1' not found.", rid));
+        return false;
+    }
+
+    Incidence::Ptr incidencePtr(incidence->clone());
+
+    Item i = item;
+    i.setMimeType(incidencePtr->mimeType());
+    i.setPayload<Incidence::Ptr>(incidencePtr);
+    itemRetrieved(i);
+    return true;
+}
+
+void ICalResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &)
+{
+    if (!checkItemAddedChanged<Incidence::Ptr>(item, CheckForAdded)) {
+        return;
+    }
+
+    Incidence::Ptr i = item.payload<Incidence::Ptr>();
+    if (!calendar()->addIncidence(Incidence::Ptr(i->clone()))) {
+        //qCritical() << "akonadi_ical_resource: Error adding incidence with uid "
+        //         << i->uid() << "; item.id() " << item.id() << i->recurrenceId();
+        cancelTask();
+        return;
+    }
+
+    Item it(item);
+    it.setRemoteId(i->instanceIdentifier());
+    scheduleWrite();
+    changeCommitted(it);
+}
+
+void ICalResource::itemChanged(const Akonadi::Item &item,
+                               const QSet<QByteArray> &parts)
+{
+    Q_UNUSED(parts)
+
+    if (!checkItemAddedChanged<Incidence::Ptr>(item, CheckForChanged)) {
+        return;
+    }
+
+    Incidence::Ptr payload = item.payload<Incidence::Ptr>();
+    Incidence::Ptr incidence = calendar()->instance(item.remoteId());
+    if (!incidence) {
+        // not in the calendar yet, should not happen -> add it
+        calendar()->addIncidence(Incidence::Ptr(payload->clone()));
+    } else {
+        // make sure any observer the resource might have installed gets properly notified
+        incidence->startUpdates();
+
+        if (incidence->type() == payload->type()) {
+            // IncidenceBase::operator= calls virtual method assign, so it's safe.
+            *incidence.staticCast<IncidenceBase>().data() = *payload.data();
+            incidence->updated();
+            incidence->endUpdates();
+        } else {
+            incidence->endUpdates();
+            qWarning() << "akonadi_ical_resource: Item changed incidence type. Replacing it.";
+
+            calendar()->deleteIncidence(incidence);
+            calendar()->addIncidence(Incidence::Ptr(payload->clone()));
+        }
+    }
+    scheduleWrite();
+    changeCommitted(item);
+}
+
+void ICalResource::doRetrieveItems(const Akonadi::Collection &col)
+{
+    Q_UNUSED(col);
+    Incidence::List incidences = calendar()->incidences();
+    Item::List items;
+    items.reserve(incidences.count());
+    foreach (const Incidence::Ptr &incidence, incidences) {
+        Item item(incidence->mimeType());
+        item.setRemoteId(incidence->instanceIdentifier());
+        item.setPayload(Incidence::Ptr(incidence->clone()));
+        items << item;
+    }
+    itemsRetrieved(items);
+}
+
+QStringList ICalResource::allMimeTypes() const
+{
+    return QStringList() << KCalCore::Event::eventMimeType()
+           << KCalCore::Todo::todoMimeType()
+           << KCalCore::Journal::journalMimeType()
+           << KCalCore::FreeBusy::freeBusyMimeType();
+}
+
+QString ICalResource::mimeType(const IncidenceBase::Ptr &incidence) const
+{
+    return incidence->mimeType();
+}
+
diff --git a/resources/ical/shared/icalresource.h b/resources/ical/shared/icalresource.h
new file mode 100644 (file)
index 0000000..3f1cc0d
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+    Copyright (c) 2006 Till Adam <adam@kde.org>
+    Copyright (c) 2009 David Jarvie <djarvie@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef ICALRESOURCE_H
+#define ICALRESOURCE_H
+
+#include "icalresourcebase.h"
+
+#include <KCalCore/IncidenceBase>
+
+class ICalResource : public ICalResourceBase
+{
+    Q_OBJECT
+
+public:
+    explicit ICalResource(const QString &id);
+    ~ICalResource();
+
+protected:
+    /**
+     * Constructor for derived classes.
+     * @param mimeTypes mimeTypes to be handled by the resource.
+     * @param icon icon name to use.
+     */
+    ICalResource(const QString &id, const QStringList &mimeTypes, const QString &icon);
+
+    bool doRetrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+    void doRetrieveItems(const Akonadi::Collection &col) Q_DECL_OVERRIDE;
+
+    void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &) Q_DECL_OVERRIDE;
+    void itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+
+    /**
+      Returns the Akonadi specific @c text/calendar sub MIME type of the given @p incidence.
+    */
+    virtual QString mimeType(const KCalCore::IncidenceBase::Ptr &incidence) const;
+
+    /**
+      Returns a list of all calendar component sub MIME types.
+     */
+    virtual QStringList allMimeTypes() const;
+};
+
+#endif
diff --git a/resources/ical/shared/icalresourcebase.cpp b/resources/ical/shared/icalresourcebase.cpp
new file mode 100644 (file)
index 0000000..47c9280
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+    Copyright (c) 2006 Till Adam <adam@kde.org>
+    Copyright (c) 2009 David Jarvie <djarvie@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "icalresourcebase.h"
+#include "icalsettingsadaptor.h"
+#include "singlefileresourceconfigdialog.h"
+
+#include <kdbusconnectionpool.h>
+
+#include <KCalCore/FileStorage>
+#include <KCalCore/MemoryCalendar>
+#include <KCalCore/Incidence>
+#include <KCalCore/ICalFormat>
+
+#include <QDebug>
+#include <KLocalizedString>
+
+using namespace Akonadi;
+using namespace KCalCore;
+using namespace SETTINGS_NAMESPACE;
+
+ICalResourceBase::ICalResourceBase(const QString &id)
+    : SingleFileResource<Settings>(id)
+{
+}
+
+void ICalResourceBase::initialise(const QStringList &mimeTypes, const QString &icon)
+{
+    setSupportedMimetypes(mimeTypes, icon);
+    new ICalSettingsAdaptor(mSettings);
+    KDBusConnectionPool::threadConnection().registerObject(QStringLiteral("/Settings"),
+            mSettings, QDBusConnection::ExportAdaptors);
+}
+
+ICalResourceBase::~ICalResourceBase()
+{
+}
+
+bool ICalResourceBase::retrieveItem(const Akonadi::Item &item,
+                                    const QSet<QByteArray> &parts)
+{
+    qDebug() << "Item:" << item.url();
+
+    if (!mCalendar) {
+        qCritical() << "akonadi_ical_resource: Calendar not loaded";
+        Q_EMIT error(i18n("Calendar not loaded."));
+        return false;
+    }
+
+    return doRetrieveItem(item, parts);
+}
+
+void ICalResourceBase::aboutToQuit()
+{
+    if (!mSettings->readOnly()) {
+        writeFile();
+    }
+    mSettings->save();
+}
+
+void ICalResourceBase::customizeConfigDialog(SingleFileResourceConfigDialog<Settings> *dlg)
+{
+    dlg->setFilter(QStringLiteral("text/calendar"));
+    dlg->setWindowTitle(i18n("Select Calendar"));
+}
+
+bool ICalResourceBase::readFromFile(const QString &fileName)
+{
+    mCalendar = KCalCore::MemoryCalendar::Ptr(new KCalCore::MemoryCalendar(QStringLiteral("UTC")));
+    mFileStorage = KCalCore::FileStorage::Ptr(new KCalCore::FileStorage(mCalendar, fileName,
+                   new KCalCore::ICalFormat()));
+    const bool result = mFileStorage->load();
+    if (!result) {
+        qCritical() << "akonadi_ical_resource: Error loading file " << fileName;
+    }
+
+    return result;
+}
+
+void ICalResourceBase::itemRemoved(const Akonadi::Item &item)
+{
+    if (!mCalendar) {
+        qCritical() << "akonadi_ical_resource: mCalendar is 0!";
+        cancelTask(i18n("Calendar not loaded."));
+        return;
+    }
+
+    Incidence::Ptr i = mCalendar->instance(item.remoteId());
+    if (i) {
+        if (!mCalendar->deleteIncidence(i)) {
+            qCritical() << "akonadi_ical_resource: Can't delete incidence with instance identifier "
+                        << item.remoteId() << "; item.id() = " << item.id();
+            cancelTask();
+            return;
+        }
+    } else {
+        qCritical() << "akonadi_ical_resource: itemRemoved(): Can't find incidence with instance identifier "
+                    << item.remoteId() << "; item.id() = " << item.id();
+    }
+    scheduleWrite();
+    changeProcessed();
+}
+
+void ICalResourceBase::retrieveItems(const Akonadi::Collection &col)
+{
+    reloadFile();
+    if (mCalendar) {
+        doRetrieveItems(col);
+    } else {
+        qCritical() << "akonadi_ical_resource: retrieveItems(): mCalendar is 0!";
+    }
+}
+
+bool ICalResourceBase::writeToFile(const QString &fileName)
+{
+    if (!mCalendar) {
+        qCritical() << "akonadi_ical_resource: writeToFile() mCalendar is 0!";
+        return false;
+    }
+
+    KCalCore::FileStorage *fileStorage = mFileStorage.data();
+    if (fileName != mFileStorage->fileName()) {
+        fileStorage = new KCalCore::FileStorage(mCalendar,
+                                                fileName,
+                                                new KCalCore::ICalFormat());
+    }
+
+    bool success = true;
+    if (!fileStorage->save()) {
+        qCritical() << QStringLiteral("akonadi_ical_resource: Failed to save calendar to file ") + fileName;
+        Q_EMIT error(i18n("Failed to save calendar file to %1", fileName));
+        success = false;
+    }
+
+    if (fileStorage != mFileStorage.data()) {
+        delete fileStorage;
+    }
+
+    return success;
+}
+
+KCalCore::MemoryCalendar::Ptr ICalResourceBase::calendar() const
+{
+    return mCalendar;
+}
+
+KCalCore::FileStorage::Ptr ICalResourceBase::fileStorage() const
+{
+    return mFileStorage;
+}
+
diff --git a/resources/ical/shared/icalresourcebase.h b/resources/ical/shared/icalresourcebase.h
new file mode 100644 (file)
index 0000000..c33fb98
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+    Copyright (c) 2006 Till Adam <adam@kde.org>
+    Copyright (c) 2009 David Jarvie <djarvie@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef ICALRESOURCEBASE_H
+#define ICALRESOURCEBASE_H
+
+#include "singlefileresource.h"
+#include "settings.h"
+
+#include <KCalCore/MemoryCalendar>
+#include <KCalCore/FileStorage>
+
+class ICalResourceBase : public Akonadi::SingleFileResource<SETTINGS_NAMESPACE::Settings>
+{
+    Q_OBJECT
+
+public:
+    explicit ICalResourceBase(const QString &id);
+    ~ICalResourceBase();
+
+protected Q_SLOTS:
+    bool retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+    void retrieveItems(const Akonadi::Collection &col) Q_DECL_OVERRIDE;
+
+protected:
+    enum CheckType { CheckForAdded, CheckForChanged };
+
+    void initialise(const QStringList &mimeTypes, const QString &icon);
+    bool readFromFile(const QString &fileName) Q_DECL_OVERRIDE;
+    bool writeToFile(const QString &fileName) Q_DECL_OVERRIDE;
+
+    /**
+     * Customize the configuration dialog before it is displayed.
+     */
+    void customizeConfigDialog(Akonadi::SingleFileResourceConfigDialog<SETTINGS_NAMESPACE::Settings> *dlg) Q_DECL_OVERRIDE;
+
+    void aboutToQuit() Q_DECL_OVERRIDE;
+
+    /**
+     * Retrieve an incidence from the calendar, and set it into a new item's payload.
+     * Retrieval of the item should be signalled by calling @p itemRetrieved().
+     * @param item the incidence ID to retrieve is provided by @c item.remoteId()
+     * @return true if item retrieved, false if not.
+     */
+    virtual bool doRetrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts) = 0;
+
+    /**
+     * Retrieve all incidences from the calendar, and set each into a new item's payload.
+     * Retrieval of the items should be signalled by calling @p itemsRetrieved().
+     */
+    virtual void doRetrieveItems(const Akonadi::Collection &col) = 0;
+
+    /**
+     * To be called at the start of derived class implementations of itemAdded()
+     * or itemChanged() to verify that required conditions are true.
+     * @param type the type of change to perform the checks for.
+     * @return true if all checks are successful, and processing can continue;
+     *         false if a check failed, in which case itemAdded() or itemChanged()
+     *               should stop processing.
+     */
+    template <typename PayloadPtr> bool checkItemAddedChanged(const Akonadi::Item &item, CheckType type);
+
+    void itemRemoved(const Akonadi::Item &item) Q_DECL_OVERRIDE;
+
+    /** Return the local calendar. */
+    KCalCore::MemoryCalendar::Ptr calendar() const;
+
+    /** Return the calendar file storage. */
+    KCalCore::FileStorage::Ptr fileStorage() const;
+
+private:
+    KCalCore::MemoryCalendar::Ptr mCalendar;
+    KCalCore::FileStorage::Ptr mFileStorage;
+};
+
+template <typename PayloadPtr>
+bool ICalResourceBase::checkItemAddedChanged(const Akonadi::Item &item, CheckType type)
+{
+    if (!mCalendar) {
+        cancelTask(i18n("Calendar not loaded."));
+        return false;
+    }
+    if (!item.hasPayload<PayloadPtr>()) {
+        QString msg = (type == CheckForAdded)
+                      ? i18n("Unable to retrieve added item %1.", item.id())
+                      : i18n("Unable to retrieve modified item %1.", item.id());
+        cancelTask(msg);
+        return false;
+    }
+    return true;
+}
+
+#endif
diff --git a/resources/ical/wizard/CMakeLists.txt b/resources/ical/wizard/CMakeLists.txt
new file mode 100644 (file)
index 0000000..945db09
--- /dev/null
@@ -0,0 +1,4 @@
+set(ICAL_FILE_DEFAULT_PATH "$HOME/.local/share/korganizer/calendar.ics")
+
+configure_file(icalwizard.es.cmake ${CMAKE_CURRENT_BINARY_DIR}/icalwizard.es)
+install ( FILES icalwizard.desktop ${CMAKE_CURRENT_BINARY_DIR}/icalwizard.es icalwizard.ui DESTINATION ${KDE_INSTALL_DATADIR}/akonadi/accountwizard/ical )
diff --git a/resources/ical/wizard/Messages.sh b/resources/ical/wizard/Messages.sh
new file mode 100644 (file)
index 0000000..6121338
--- /dev/null
@@ -0,0 +1,4 @@
+#! /usr/bin/env bash
+$EXTRACTRC *.ui >> rc.cpp
+$XGETTEXT *.cpp -o $podir/accountwizard_ical.pot
+$XGETTEXT -kqsTr *.es.cmake -j -o $podir/accountwizard_ical.pot
diff --git a/resources/ical/wizard/icalwizard.desktop b/resources/ical/wizard/icalwizard.desktop
new file mode 100644 (file)
index 0000000..40049ad
--- /dev/null
@@ -0,0 +1,105 @@
+[Desktop Entry]
+Name=ICal Calendar File
+Name[ar]=ملف تقويم ICal
+Name[bg]=Календарен файл iCal
+Name[bs]=ICal kalendar datoteka
+Name[ca]=Fitxer de calendari ICal
+Name[ca@valencia]=Fitxer de calendari ICal
+Name[cs]=Soubor s kalendářem iCal
+Name[da]=iCal-kalenderfil
+Name[de]=ICal-Kalenderdatei
+Name[el]=Αρχείο ημερολογίου ICal
+Name[en_GB]=ICal Calendar File
+Name[es]=Archivo de calendario ICal
+Name[et]=ICali kalendrifail
+Name[fi]=iCal-kalenteritiedosto
+Name[fr]=Fichier d'agenda au format « ICal »
+Name[ga]=Comhad Féilire ICal
+Name[gl]=Ficheiro de Calendario de ICal
+Name[hu]=iCal-naptárfájl
+Name[ia]=File iCal de calendario
+Name[it]=Calendario iCal
+Name[ja]=iCal カレンダーファイル
+Name[kk]=ICal күнтізбе файлы
+Name[km]=ឯកសារ​ប្រតិទិន ICal
+Name[ko]=ICal 달력 파일
+Name[lt]=ICal kalendoriaus failas
+Name[lv]=ICal kalendāra fails
+Name[nb]=ICal kalenderfil
+Name[nds]=ICal-Kalennerdatei
+Name[nl]=ICal-agendabestand
+Name[nn]=iCal-kalenderfil
+Name[pa]=ICal ਕੈਲੰਡਰ ਫਾਇਲ
+Name[pl]=Plik kalendarza ICal
+Name[pt]=Ficheiro de Calendário ICal
+Name[pt_BR]=Arquivo de calendário ICal
+Name[ro]=Fișier-calendar ICal
+Name[ru]=Файл календаря iCal
+Name[sk]=Súbor kalendára iCal
+Name[sl]=Koledarska datoteka iCal
+Name[sr]=И‑календарски фајл
+Name[sr@ijekavian]=И‑календарски фајл
+Name[sr@ijekavianlatin]=I‑kalendarski fajl
+Name[sr@latin]=I‑kalendarski fajl
+Name[sv]=ICal-kalenderfil
+Name[tr]=ICal Takvim Dosyası
+Name[uk]=Файл календаря ICal
+Name[x-test]=xxICal Calendar Filexx
+Name[zh_CN]=ICal 日历文件
+Name[zh_TW]=ICal 行事曆檔案
+Icon=text-calendar
+Comment=Loads data from an iCal file
+Comment[ar]=تحميل البيانات من ملف ICal
+Comment[bg]=Зареждане на данни от файл iCal
+Comment[bs]=Učitava podatke iz iCal datoteke
+Comment[ca]=Carrega les dades des d'un fitxer iCal
+Comment[ca@valencia]=Carrega les dades des d'un fitxer iCal
+Comment[cs]=Načítá data z iCal souboru
+Comment[da]=Indlæser data fra en iCal-fil
+Comment[de]=Daten werden aus einer lokalen iCal-Datei geladen
+Comment[el]=Φόρτωση δεδομένων από ένα αρχείο iCal
+Comment[en_GB]=Loads data from an iCal file
+Comment[es]=Carga datos de un archivo iCal local
+Comment[et]=Andmete laadimine iCal-failist
+Comment[fi]=Noutaa tietoa iCal-tiedostosta
+Comment[fr]=Charge des données depuis un fichier au format « iCal »
+Comment[ga]=Breiseán a luchtaíonn sonraí ó chomhad iCal
+Comment[gl]=Carga datos desde un ficheiro iCal
+Comment[hu]=Adatbetöltés iCal formátumú naptárból
+Comment[ia]=Lege datos de un file iCal
+Comment[it]=Carica dati da un file iCal
+Comment[ja]=iCal ファイルからデータを読み込みます
+Comment[kk]=ICal күнтізбе файлынан деректі алып беру
+Comment[km]=ផ្ទុក​ទិន្នន័យ​ពី​ឯកសារ iCal
+Comment[ko]=로컬 iCal 파일에서 데이터를 가져오기
+Comment[lt]=Įkelia duomenis iš iCal failo
+Comment[lv]=Ielādē datus no iCal faila
+Comment[nb]=Laster data fra en iCal-fil
+Comment[nds]=Laadt Daten ut en ICal-Datei
+Comment[nl]=Laadt gegevens van een iCal-bestand
+Comment[nn]=Lastar data frå ei iCal-fil
+Comment[pa]=iCal ਫਾਇਲ ਤੋਂ ਡਾਟਾ ਡਾਊਨਲੋਡ ਕਰੋ
+Comment[pl]=Wczytuje dane z pliku ICal
+Comment[pt]=Carrega os dados de um ficheiro iCal
+Comment[pt_BR]=Carrega os dados de um arquivo iCal
+Comment[ro]=Încarcă date dintr-un fișier iCal
+Comment[ru]=Загрузка данных из файла календаря iCal
+Comment[sk]=Načíta dáta zo súboru iCal
+Comment[sl]=Naloži podatke iz datoteke iCal
+Comment[sr]=Учитава податке из и‑календарског фајла
+Comment[sr@ijekavian]=Учитава податке из и‑календарског фајла
+Comment[sr@ijekavianlatin]=Učitava podatke iz i‑kalendarskog fajla
+Comment[sr@latin]=Učitava podatke iz i‑kalendarskog fajla
+Comment[sv]=Laddar data från en iCal-fil
+Comment[tr]=iCal dosyasından veri yükler
+Comment[uk]=Завантажує дані з файла iCal
+Comment[x-test]=xxLoads data from an iCal filexx
+Comment[zh_CN]=从 iCal 文件载入数据
+Comment[zh_TW]=從 iCal 檔載入資料
+
+[Wizard]
+Type=text/calendar
+Script=icalwizard.es
+
+[Translate]
+Filename=accountwizard_ical
diff --git a/resources/ical/wizard/icalwizard.es.cmake b/resources/ical/wizard/icalwizard.es.cmake
new file mode 100644 (file)
index 0000000..c2771bb
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+    Copyright (c) 2010 Till Adam <adam@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+var page = Dialog.addPage( "icalwizard.ui", qsTr("Settings") );
+
+page.widget().lineEdit.text = "${ICAL_FILE_DEFAULT_PATH}";
+
+function validateInput()
+{
+  if ( page.widget().lineEdit.text == "" ) {
+    page.setValid( false );
+  } else {
+    page.setValid( true );
+  }
+}
+
+function setup()
+{
+  var icalRes = SetupManager.createResource( "akonadi_ical_resource" );
+  icalRes.setOption( "Path", page.widget().lineEdit.text );
+  icalRes.setName( qsTr("Default Calendar") );
+  SetupManager.execute();
+}
+
+page.widget().lineEdit.textChanged.connect( validateInput );
+page.pageLeftNext.connect( setup );
+validateInput();
diff --git a/resources/ical/wizard/icalwizard.ui b/resources/ical/wizard/icalwizard.ui
new file mode 100644 (file)
index 0000000..7307df2
--- /dev/null
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>icalWizard</class>
+ <widget class="QWidget" name="icalWizard">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>300</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QLabel" name="label">
+       <property name="text">
+        <string>Filename:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QLineEdit" name="lineEdit"/>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <spacer name="verticalSpacer_2">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>138</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/resources/icaldir/CMakeLists.txt b/resources/icaldir/CMakeLists.txt
new file mode 100644 (file)
index 0000000..ea5b9ec
--- /dev/null
@@ -0,0 +1,36 @@
+
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_icaldir_resource\")
+
+
+########### next target ###############
+
+set( icaldirresource_SRCS
+  icaldirresource.cpp
+  dirsettingsdialog.cpp
+)
+
+ki18n_wrap_ui(icaldirresource_SRCS settingsdialog.ui)
+kconfig_add_kcfg_files(icaldirresource_SRCS settings.kcfgc)
+kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/icaldirresource.kcfg org.kde.Akonadi.ICalDirectory.Settings)
+qt5_add_dbus_adaptor(icaldirresource_SRCS
+  ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.ICalDirectory.Settings.xml settings.h Settings
+)
+
+install( FILES icaldirresource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents" )
+
+add_executable(akonadi_icaldir_resource ${icaldirresource_SRCS})
+
+if( APPLE )
+  set_target_properties(akonadi_icaldir_resource PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../Info.plist.template)
+  set_target_properties(akonadi_icaldir_resource PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.ICalDirectory")
+  set_target_properties(akonadi_icaldir_resource PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi ICalDirectory Resource")
+endif ()
+
+
+target_link_libraries(akonadi_icaldir_resource
+  KF5::AkonadiCore
+  KF5::AkonadiAgentBase  
+  KF5::CalendarCore
+)
+
+install(TARGETS akonadi_icaldir_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
diff --git a/resources/icaldir/Messages.sh b/resources/icaldir/Messages.sh
new file mode 100644 (file)
index 0000000..4329512
--- /dev/null
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+$EXTRACTRC `find . -name \*.ui` `find . -name \*.kcfg` >> rc.cpp || exit 11
+$XGETTEXT *.cpp -o $podir/akonadi_icaldir_resource.pot
diff --git a/resources/icaldir/dirsettingsdialog.cpp b/resources/icaldir/dirsettingsdialog.cpp
new file mode 100644 (file)
index 0000000..b75413e
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "dirsettingsdialog.h"
+
+#include "settings.h"
+
+#include <KConfigDialogManager>
+#include <KWindowSystem>
+#include <KLocalizedString>
+#include <QUrl>
+
+#include <QTimer>
+#include <QDialogButtonBox>
+#include <QPushButton>
+#include <QVBoxLayout>
+
+using namespace Akonadi;
+
+SettingsDialog::SettingsDialog(WId windowId)
+    : QDialog()
+{
+    QWidget *mainWidget = new QWidget(this);
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    setLayout(mainLayout);
+    mainLayout->addWidget(mainWidget);
+    ui.setupUi(mainWidget);
+    ui.kcfg_Path->setMode(KFile::LocalOnly | KFile::Directory);
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    mOkButton = buttonBox->button(QDialogButtonBox::Ok);
+    mOkButton->setDefault(true);
+    mOkButton->setShortcut(Qt::CTRL | Qt::Key_Return);
+    connect(buttonBox, &QDialogButtonBox::accepted, this, &SettingsDialog::accept);
+    connect(buttonBox, &QDialogButtonBox::rejected, this, &SettingsDialog::reject);
+    mainLayout->addWidget(buttonBox);
+
+    if (windowId) {
+        KWindowSystem::setMainWindow(this, windowId);
+    }
+
+    connect(mOkButton, &QPushButton::clicked, this, &SettingsDialog::save);
+
+    connect(ui.kcfg_Path, &KUrlRequester::textChanged, this, &SettingsDialog::validate);
+    connect(ui.kcfg_ReadOnly, &QCheckBox::toggled, this, &SettingsDialog::validate);
+
+    QTimer::singleShot(0, this, &SettingsDialog::validate);
+
+    ui.kcfg_Path->setUrl(QUrl::fromLocalFile(Settings::self()->path()));
+    ui.kcfg_AutosaveInterval->setSuffix(ki18np(" minute", " minutes"));
+    mManager = new KConfigDialogManager(this, Settings::self());
+    mManager->updateWidgets();
+}
+
+void SettingsDialog::save()
+{
+    mManager->updateSettings();
+    Settings::self()->setPath(ui.kcfg_Path->url().toLocalFile());
+    Settings::self()->save();
+}
+
+void SettingsDialog::validate()
+{
+    const QUrl currentUrl = ui.kcfg_Path->url();
+    if (currentUrl.isEmpty()) {
+        mOkButton->setEnabled(false);
+        return;
+    }
+
+    const QFileInfo file(currentUrl.toLocalFile());
+    if (file.exists() && !file.isWritable()) {
+        ui.kcfg_ReadOnly->setEnabled(false);
+        ui.kcfg_ReadOnly->setChecked(true);
+    } else {
+        ui.kcfg_ReadOnly->setEnabled(true);
+    }
+    mOkButton->setEnabled(true);
+}
diff --git a/resources/icaldir/dirsettingsdialog.h b/resources/icaldir/dirsettingsdialog.h
new file mode 100644 (file)
index 0000000..c56f495
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef DIRSETTINGSDIALOG_H
+#define DIRSETTINGSDIALOG_H
+
+#include "ui_settingsdialog.h"
+
+#include <QDialog>
+#include <QPushButton>
+
+class KConfigDialogManager;
+
+namespace Akonadi
+{
+
+class SettingsDialog : public QDialog
+{
+    Q_OBJECT
+public:
+    explicit SettingsDialog(WId windowId);
+
+private Q_SLOTS:
+    void save();
+    void validate();
+
+private:
+    Ui::SettingsDialog ui;
+    KConfigDialogManager *mManager;
+    QPushButton *mOkButton;
+};
+
+}
+
+#endif
diff --git a/resources/icaldir/icaldirresource.cpp b/resources/icaldir/icaldirresource.cpp
new file mode 100644 (file)
index 0000000..358ec5a
--- /dev/null
@@ -0,0 +1,315 @@
+/*
+    Copyright (c) 2008 Tobias Koenig <tokoe@kde.org>
+    Copyright (c) 2008 Bertjan Broeksema <broeksema@kde.org>
+    Copyright (c) 2012 Sérgio Martins <iamsergio@gmail.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "icaldirresource.h"
+
+#include "settingsadaptor.h"
+#include "dirsettingsdialog.h"
+
+#include <changerecorder.h>
+#include <entitydisplayattribute.h>
+#include <itemfetchscope.h>
+
+#include <KCalCore/MemoryCalendar>
+#include <KCalCore/FileStorage>
+#include <KCalCore/ICalFormat>
+#include <QIcon>
+#include <KLocalizedString>
+#include <QDebug>
+
+#include <QtCore/QDir>
+#include <QtCore/QDirIterator>
+#include <QtCore/QFile>
+
+using namespace Akonadi;
+using namespace KCalCore;
+
+static Incidence::Ptr readFromFile(const QString &fileName, const QString &expectedIdentifier)
+{
+    MemoryCalendar::Ptr calendar = MemoryCalendar::Ptr(new MemoryCalendar(QStringLiteral("UTC")));
+    FileStorage::Ptr fileStorage = FileStorage::Ptr(new FileStorage(calendar, fileName, new ICalFormat()));
+
+    Incidence::Ptr incidence;
+    if (fileStorage->load()) {
+        Incidence::List incidences = calendar->incidences();
+        if (incidences.count() == 1 && incidences.first()->instanceIdentifier() == expectedIdentifier) {
+            incidence = incidences.first();
+        }
+    } else {
+        qCritical() << "Error loading file " << fileName;
+    }
+
+    return incidence;
+}
+
+static bool writeToFile(const QString &fileName, Incidence::Ptr &incidence)
+{
+    if (!incidence) {
+        qCritical() << "incidence is 0!";
+        return false;
+    }
+
+    MemoryCalendar::Ptr calendar = MemoryCalendar::Ptr(new MemoryCalendar(QStringLiteral("UTC")));
+    FileStorage::Ptr fileStorage = FileStorage::Ptr(new FileStorage(calendar, fileName, new ICalFormat()));
+    calendar->addIncidence(incidence);
+    Q_ASSERT(calendar->incidences().count() == 1);
+
+    const bool success = fileStorage->save();
+    if (!success) {
+        qCritical() << QStringLiteral("Failed to save calendar to file ") + fileName;
+    }
+
+    return success;
+}
+
+ICalDirResource::ICalDirResource(const QString &id)
+    : ResourceBase(id)
+{
+    // setup the resource
+    new SettingsAdaptor(Settings::self());
+    QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"),
+            Settings::self(), QDBusConnection::ExportAdaptors);
+
+    changeRecorder()->itemFetchScope().fetchFullPayload();
+}
+
+ICalDirResource::~ICalDirResource()
+{
+}
+
+void ICalDirResource::aboutToQuit()
+{
+    Settings::self()->save();
+}
+
+void ICalDirResource::configure(WId windowId)
+{
+    SettingsDialog dlg(windowId);
+    dlg.setWindowIcon(QIcon::fromTheme(QStringLiteral("text-calendar")));
+    if (dlg.exec()) {
+        initializeICalDirectory();
+        loadIncidences();
+
+        synchronize();
+
+        Q_EMIT configurationDialogAccepted();
+    } else {
+        Q_EMIT configurationDialogRejected();
+    }
+}
+
+bool ICalDirResource::loadIncidences()
+{
+    mIncidences.clear();
+
+    QDirIterator it(iCalDirectoryName());
+    while (it.hasNext()) {
+        it.next();
+        if (it.fileName() != QLatin1String(".") && it.fileName() != QLatin1String("..") && it.fileName() != QLatin1String("WARNING_README.txt")) {
+            const KCalCore::Incidence::Ptr incidence = readFromFile(it.filePath(), it.fileName());
+            if (incidence) {
+                mIncidences.insert(incidence->instanceIdentifier(), incidence);
+            }
+        }
+    }
+
+    Q_EMIT status(Idle);
+    return true;
+}
+
+bool ICalDirResource::retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &)
+{
+    const QString remoteId = item.remoteId();
+    if (!mIncidences.contains(remoteId)) {
+        Q_EMIT error(i18n("Incidence with uid '%1' not found.", remoteId));
+        return false;
+    }
+
+    Item newItem(item);
+    newItem.setPayload<KCalCore::Incidence::Ptr>(mIncidences.value(remoteId));
+    itemRetrieved(newItem);
+
+    return true;
+}
+
+void ICalDirResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &)
+{
+    if (Settings::self()->readOnly()) {
+        Q_EMIT error(i18n("Trying to write to a read-only directory: '%1'", iCalDirectoryName()));
+        cancelTask();
+        return;
+    }
+
+    KCalCore::Incidence::Ptr incidence;
+    if (item.hasPayload<KCalCore::Incidence::Ptr>()) {
+        incidence = item.payload<KCalCore::Incidence::Ptr>();
+    }
+
+    if (incidence) {
+        // add it to the cache...
+        mIncidences.insert(incidence->instanceIdentifier(), incidence);
+
+        // ... and write it through to the file system
+        const bool success = writeToFile(iCalDirectoryFileName(incidence->instanceIdentifier()), incidence);
+
+        if (success) {
+            // report everything ok
+            Item newItem(item);
+            newItem.setRemoteId(incidence->instanceIdentifier());
+            changeCommitted(newItem);
+        } else {
+            cancelTask();
+        }
+    } else {
+        changeProcessed();
+    }
+}
+
+void ICalDirResource::itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &)
+{
+    if (Settings::self()->readOnly()) {
+        Q_EMIT error(i18n("Trying to write to a read-only directory: '%1'", iCalDirectoryName()));
+        cancelTask();
+        return;
+    }
+
+    KCalCore::Incidence::Ptr incidence;
+    if (item.hasPayload<KCalCore::Incidence::Ptr>()) {
+        incidence  = item.payload<KCalCore::Incidence::Ptr>();
+    }
+
+    if (incidence) {
+        // change it in the cache...
+        mIncidences.insert(incidence->instanceIdentifier(), incidence);
+
+        // ... and write it through to the file system
+        const bool success = writeToFile(iCalDirectoryFileName(incidence->instanceIdentifier()), incidence);
+
+        if (success) {
+            Item newItem(item);
+            newItem.setRemoteId(incidence->instanceIdentifier());
+            changeCommitted(newItem);
+        } else {
+            cancelTask();
+        }
+    } else {
+        changeProcessed();
+    }
+}
+
+void ICalDirResource::itemRemoved(const Akonadi::Item &item)
+{
+    if (Settings::self()->readOnly()) {
+        Q_EMIT error(i18n("Trying to write to a read-only directory: '%1'", iCalDirectoryName()));
+        cancelTask();
+        return;
+    }
+
+    // remove it from the cache...
+    if (mIncidences.contains(item.remoteId())) {
+        mIncidences.remove(item.remoteId());
+    }
+
+    // ... and remove it from the file system
+    QFile::remove(iCalDirectoryFileName(item.remoteId()));
+
+    changeProcessed();
+}
+
+void ICalDirResource::retrieveCollections()
+{
+    Collection c;
+    c.setParentCollection(Collection::root());
+    c.setRemoteId(iCalDirectoryName());
+    c.setName(name());
+
+    QStringList mimetypes;
+    mimetypes << KCalCore::Event::eventMimeType() << KCalCore::Todo::todoMimeType() << KCalCore::Journal::journalMimeType() << QStringLiteral("text/calendar");
+    c.setContentMimeTypes(mimetypes);
+
+    if (Settings::self()->readOnly()) {
+        c.setRights(Collection::CanChangeCollection);
+    } else {
+        Collection::Rights rights = Collection::ReadOnly;
+        rights |= Collection::CanChangeItem;
+        rights |= Collection::CanCreateItem;
+        rights |= Collection::CanDeleteItem;
+        rights |= Collection::CanChangeCollection;
+        c.setRights(rights);
+    }
+
+    EntityDisplayAttribute *attr = c.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
+    attr->setDisplayName(i18n("Calendar Folder"));
+    attr->setIconName(QStringLiteral("office-calendar"));
+
+    Collection::List list;
+    list << c;
+    collectionsRetrieved(list);
+}
+
+void ICalDirResource::retrieveItems(const Akonadi::Collection &)
+{
+    loadIncidences();
+    Item::List items;
+    items.reserve(mIncidences.count());
+
+    foreach (const KCalCore::Incidence::Ptr &incidence, mIncidences) {
+        Item item;
+        item.setRemoteId(incidence->instanceIdentifier());
+        item.setMimeType(incidence->mimeType());
+        items.append(item);
+    }
+
+    itemsRetrieved(items);
+}
+
+QString ICalDirResource::iCalDirectoryName() const
+{
+    return Settings::self()->path();
+}
+
+QString ICalDirResource::iCalDirectoryFileName(const QString &file) const
+{
+    return Settings::self()->path() + QDir::separator() + file;
+}
+
+void ICalDirResource::initializeICalDirectory() const
+{
+    QDir dir(iCalDirectoryName());
+
+    // if folder does not exists, create it
+    if (!dir.exists()) {
+        QDir::root().mkpath(dir.absolutePath());
+    }
+
+    // check whether warning file is in place...
+    QFile file(dir.absolutePath() + QDir::separator() + QStringLiteral("WARNING_README.txt"));
+    if (!file.exists()) {
+        // ... if not, create it
+        file.open(QIODevice::WriteOnly);
+        file.write("Important Warning!!!\n\n"
+                   "Don't create or copy files inside this folder manually, they are managed by the Akonadi framework!\n");
+        file.close();
+    }
+}
+
+AKONADI_RESOURCE_MAIN(ICalDirResource)
+
diff --git a/resources/icaldir/icaldirresource.desktop b/resources/icaldir/icaldirresource.desktop
new file mode 100644 (file)
index 0000000..5a9974b
--- /dev/null
@@ -0,0 +1,89 @@
+[Desktop Entry]
+Name=ICal Calendar Folder
+Name[bg]=Календарна папка iCal
+Name[bs]=ICal kalendarska datoteka
+Name[ca]=Carpeta de calendari ICal
+Name[ca@valencia]=Carpeta de calendari ICal
+Name[cs]=Složka iCal kalendáře
+Name[da]=iCal-kalendermappe
+Name[de]=ICal-Kalenderordner
+Name[el]=Φάκελος ημερολογίου ICal
+Name[en_GB]=ICal Calendar Folder
+Name[es]=Carpeta de calendario ICal
+Name[et]=ICali kalendrikataloog
+Name[fi]=iCal-kalenterikansio
+Name[fr]=Dossier d'agendas au format « ICal »
+Name[ga]=Fillteán Féilire ICal
+Name[gl]=Cartafol de calendario iCal
+Name[hu]=ICal naptármappa
+Name[ia]=Dossier de calendario iCal
+Name[it]=Cartella di calendario iCal
+Name[kk]=ICal күнтізбе қапшығы
+Name[ko]=ICal 달력 폴더
+Name[lt]=ICal kalendoriaus aplankas
+Name[nb]=ICal kalendermappe
+Name[nds]=ICal-Kalennerorner
+Name[nl]=ICal-agendamap
+Name[pl]=Folder kalendarza ICal
+Name[pt]=Pasta de Calendários ICal
+Name[pt_BR]=Pasta de calendário ICal
+Name[ru]=Каталог календаря iCal
+Name[sk]=Priečinok kalendára iCal
+Name[sl]=Koledarska mapa iCal
+Name[sr]=И‑календарски фасцикла
+Name[sr@ijekavian]=И‑календарски фасцикла
+Name[sr@ijekavianlatin]=I‑kalendarski fascikla
+Name[sr@latin]=I‑kalendarski fascikla
+Name[sv]=ICal-kalenderkatalog
+Name[tr]=ICal Takvim Klasörü
+Name[uk]=Тека календаря ICal
+Name[x-test]=xxICal Calendar Folderxx
+Name[zh_CN]=ICal 日历文件夹
+Name[zh_TW]=ICal 行事曆資料夾
+Comment="Provides access to calendar items, each stored in a single file, in a given directory"
+Comment[bs]="Puža pristup stavkama kalendara, svaka u pojedinoh datoteci u datom direktoriju"
+Comment[ca]=«Proporciona accés als elements d'un calendari, cadascun emmagatzemat en un fitxer individual, en un directori proporcionat»
+Comment[ca@valencia]=«Proporciona accés als elements d'un calendari, cadascun emmagatzemat en un fitxer individual, en un directori proporcionat»
+Comment[cs]="Poskytuje přístup k položkám kalendáře, každé uložené v jednom souboru v daném adresáři"
+Comment[da]="Giver adgang til en kalenderelementer, hver især gemt i en enkelt fil i en given mappe"
+Comment[de]="Ermöglicht Zugriff auf Kalender, die jeweils in einzelnen Dateien in einem vorgegebenen Ordner gespeichert sind"
+Comment[el]="Παρέχει πρόσβαση σε αντικείμενα ημερολογίου, με το καθένα αποθηκευμένο σε ένα αρχείο, σε δοσμένο κατάλογο"
+Comment[en_GB]="Provides access to calendar items, each stored in a single file, in a given directory"
+Comment[es]=«Proporciona acceso a elementos de calendario, cada uno almacenado en un solo archivo, en un directorio dado»
+Comment[et]="Võimaldab kasutada eraldi failidesse salvestatud kalendrielemente määratud kataloogis"
+Comment[fi]="Tarjoaa pääsyn määräkansion yksittäisiin tiedostoihin tallennettuihin kalenterimerkintöihin"
+Comment[fr]=« Fournit l'accès aux éléments d'agenda, chacun stocké dans un fichier distinct, dans un dossier donné »
+Comment[gl]=«Fornece acceso a elementos de calendario, cada un almacenado nun ficheiro de seu, nun directorio dado.»
+Comment[hu]=„Hozzáférést biztosít a naptárelemekhez, mindegyiket külön fájlban tárolva a megadott könyvtárban”
+Comment[ia]="Il provide accesso a elementos de calendario, cata un immagazinate in un file singule, in un date directorio"
+Comment[it]="Fornisce accesso a voci di calendario, ciascuna memorizzata in un singolo file in una data cartella"
+Comment[kk]="Көрсетілген қапшықта, бөлек файлдарда сақталған күнтізбенің жазуларына қатынау мүмкіндігін береді"
+Comment[ko]="지정한 디렉터리의 단일 로컬 파일에 저장되어 있는 달력 항목에 접근하기"
+Comment[lt]=„Suteikia prieigą prie kalendoriaus įrašų, kurie saugomi individualiai nurodytame kataloge“
+Comment[nb]=«Gir tilgang til kalenderelementer, hver lagret i én enkelt fil, i en gitt mappe»
+Comment[nds]=„Stellt Togriep op Kalennerindrääg praat, elkeen binnen een Datei binnen en angeven Orner“
+Comment[nl]="Geeft toegang tot agenda-items, elk opgeslagen in een enkel bestand in een opgegeven map"
+Comment[pl]="Daje dostęp do plików obiektów kalendarza, każdy w oddzielnym pliku, w podanym katalogu"
+Comment[pt]=Oferece o acesso aos itens do calendário, estando cada um guardado num único ficheiro de uma dada pasta
+Comment[pt_BR]="Fornece acesso aos itens do calendário, cada um armazenado em um único arquivo na pasta indicada"
+Comment[ru]="Обеспечивает доступ к элементам календаря, каждый из которых хранится в отдельном файле в указанном каталоге"
+Comment[sk]="Poskytuje prístup do položiek kalendára, každá uložená v jednom súbore v danom adresári"
+Comment[sl]=»Nudi dostop do koledarskih vnosov, ki so shranjeni vsak posebej v svoji datoteki, v dani mapi«
+Comment[sr]="Омогућава приступ календарским ставкама, складиштеним у по једном фајлу у датој фасцикли"
+Comment[sr@ijekavian]="Омогућава приступ календарским ставкама, складиштеним у по једном фајлу у датој фасцикли"
+Comment[sr@ijekavianlatin]="Omogućava pristup kalendarskim stavkama, skladištenim u po jednom fajlu u datoj fascikli"
+Comment[sr@latin]="Omogućava pristup kalendarskim stavkama, skladištenim u po jednom fajlu u datoj fascikli"
+Comment[sv]="Ger tillgång till kalenderobjekt, vart och ett lagrat i en enda fil i en angiven katalog"
+Comment[tr]="Belirtilen dizinde tek bir yerel dosyada depolanmış bir alarm takvimine erişim sağlar"
+Comment[uk]="Надає доступ до записів календаря, кожен з яких зберігається у окремому файлі у вказаному каталозі"
+Comment[x-test]=xx"Provides access to calendar items, each stored in a single file, in a given directory"xx
+Comment[zh_CN]=“提供日历内容的访问支持,每个日历内容都存储在指定目录的单独文件中”
+Comment[zh_TW]="提供存取行事曆的項目。每個項目都以單一檔案存放在指定的目錄中"
+
+Type=AkonadiResource
+Exec=akonadi_icaldir_resource
+Icon=text-calendar
+
+X-Akonadi-MimeTypes=application/x-vnd.akonadi.calendar.event,application/x-vnd.akonadi.calendar.todo,application/x-vnd.akonadi.calendar.journal,application/x-vnd.akonadi.calendar.freebusy
+X-Akonadi-Capabilities=Resource
+X-Akonadi-Identifier=akonadi_icaldir_resource
diff --git a/resources/icaldir/icaldirresource.h b/resources/icaldir/icaldirresource.h
new file mode 100644 (file)
index 0000000..7eb9961
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+    Copyright (c) 2008 Tobias Koenig <tokoe@kde.org>
+    Copyright (c) 2012 Sérgio Martins <iamsergio@gmail.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef ICALDIRRESOURCE_H
+#define ICALDIRRESOURCE_H
+
+#include <resourcebase.h>
+
+#include <KCalCore/Incidence>
+
+#include <QHash>
+
+class ICalDirResource : public Akonadi::ResourceBase, public Akonadi::AgentBase::Observer
+{
+    Q_OBJECT
+
+public:
+    explicit ICalDirResource(const QString &id);
+    ~ICalDirResource();
+
+public Q_SLOTS:
+    void configure(WId windowId) Q_DECL_OVERRIDE;
+    void aboutToQuit() Q_DECL_OVERRIDE;
+
+protected Q_SLOTS:
+    void retrieveCollections() Q_DECL_OVERRIDE;
+    void retrieveItems(const Akonadi::Collection &col) Q_DECL_OVERRIDE;
+    bool retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+
+protected:
+    void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    void itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+    void itemRemoved(const Akonadi::Item &item) Q_DECL_OVERRIDE;
+
+private:
+    bool loadIncidences();
+    QString iCalDirectoryName() const;
+    QString iCalDirectoryFileName(const QString &file) const;
+    void initializeICalDirectory() const;
+
+private:
+    QHash<QString, KCalCore::Incidence::Ptr> mIncidences;
+};
+
+#endif
diff --git a/resources/icaldir/icaldirresource.kcfg b/resources/icaldir/icaldirresource.kcfg
new file mode 100644 (file)
index 0000000..de6b3d6
--- /dev/null
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:kcfg="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
+      http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
+  <kcfgfile/>
+  <group name="General">
+    <entry name="Path" type="Path">
+      <label>Path to iCal directory</label>
+      <default></default>
+    </entry>
+    <entry name="AutosaveInterval" type="UInt">
+      <label>Autosave interval time (in minutes).</label>
+      <default>5</default>
+    </entry>
+    <entry name="ReadOnly" type="Bool">
+      <label>Do not change the actual backend data.</label>
+      <default>false</default>
+    </entry>
+  </group>
+</kcfg>
diff --git a/resources/icaldir/settings.kcfgc b/resources/icaldir/settings.kcfgc
new file mode 100644 (file)
index 0000000..4fea847
--- /dev/null
@@ -0,0 +1,7 @@
+File=icaldirresource.kcfg
+ClassName=Settings
+Mutators=true
+ItemAccessors=true
+SetUserTexts=true
+Singleton=true
+GlobalEnums=true
diff --git a/resources/icaldir/settingsdialog.ui b/resources/icaldir/settingsdialog.ui
new file mode 100644 (file)
index 0000000..7773174
--- /dev/null
@@ -0,0 +1,229 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SettingsDialog</class>
+ <widget class="QWidget" name="SettingsDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>547</width>
+    <height>386</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QTabWidget" name="ktabwidget">
+     <property name="currentIndex">
+      <number>0</number>
+     </property>
+     <widget class="QWidget" name="tab">
+      <attribute name="title">
+       <string>Directory</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_4">
+       <item>
+        <widget class="QGroupBox" name="groupBox_2">
+         <property name="title">
+          <string>Directory Name</string>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_3">
+          <item>
+           <layout class="QHBoxLayout" name="horizontalLayout">
+            <item>
+             <widget class="QLabel" name="label">
+              <property name="text">
+               <string>Di&amp;rectory:</string>
+              </property>
+              <property name="buddy">
+               <cstring>kcfg_Path</cstring>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="KUrlRequester" name="kcfg_Path"/>
+            </item>
+           </layout>
+          </item>
+          <item>
+           <widget class="QLabel" name="label_3">
+            <property name="text">
+             <string>Select the directory whose contents should be represented by this resource. If the directory does not exist, it will be created.</string>
+            </property>
+            <property name="wordWrap">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <widget class="QGroupBox" name="groupBox">
+         <property name="title">
+          <string>Access Rights</string>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_2">
+          <item>
+           <widget class="QCheckBox" name="kcfg_ReadOnly">
+            <property name="text">
+             <string>Read only</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QLabel" name="label_2">
+            <property name="text">
+             <string>If read-only mode is enabled, no changes will be written to the directory selected above. Read-only mode will be automatically enabled if you do not have write access to the directory.</string>
+            </property>
+            <property name="wordWrap">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <spacer name="verticalSpacer">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>4</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tab_2">
+      <attribute name="title">
+       <string>Tuning</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_5">
+       <item>
+        <widget class="QLabel" name="label_4">
+         <property name="text">
+          <string>The options on this page allow you to change parameters that balance data safety and consistency against performance. In general you should be careful with changing anything here, the defaults are good enough in most cases.</string>
+         </property>
+         <property name="wordWrap">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout_2">
+         <item>
+          <widget class="QLabel" name="autosaveLabel">
+           <property name="text">
+            <string>Autosave delay:</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="KPluralHandlingSpinBox" name="kcfg_AutosaveInterval">
+           <property name="minimum">
+            <number>0</number>
+           </property>
+           <property name="value">
+            <number>1</number>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <spacer name="horizontalSpacer">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>40</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+        </layout>
+       </item>
+       <item>
+        <spacer name="verticalSpacer_2">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>138</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KUrlRequester</class>
+   <extends>QFrame</extends>
+   <header>kurlrequester.h</header>
+   <container>1</container>
+  </customwidget>
+  <customwidget>
+   <class>KPluralHandlingSpinBox</class>
+   <extends>QSpinBox</extends>
+   <header>KPluralHandlingSpinBox</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>kcfg_ReadOnly</sender>
+   <signal>toggled(bool)</signal>
+   <receiver>kcfg_AutosaveInterval</receiver>
+   <slot>setDisabled(bool)</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>273</x>
+     <y>205</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>101</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>kcfg_ReadOnly</sender>
+   <signal>toggled(bool)</signal>
+   <receiver>autosaveLabel</receiver>
+   <slot>setDisabled(bool)</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>273</x>
+     <y>205</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>56</x>
+     <y>101</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
diff --git a/resources/imap/CMakeLists.txt b/resources/imap/CMakeLists.txt
new file mode 100644 (file)
index 0000000..9df182d
--- /dev/null
@@ -0,0 +1,130 @@
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_imap_resource\")
+
+########### next target ###############
+
+set( imapresource_LIB_SRCS
+  addcollectiontask.cpp
+  additemtask.cpp
+  batchfetcher.cpp
+  changecollectiontask.cpp
+  changeitemsflagstask.cpp
+  changeitemtask.cpp
+  expungecollectiontask.cpp
+  highestmodseqattribute.cpp
+  imapaccount.cpp
+  imapflags.cpp
+  imapresourcebase.cpp
+  messagehelper.cpp
+  movecollectiontask.cpp
+  moveitemstask.cpp
+  noselectattribute.cpp
+  noinferiorsattribute.cpp
+  passwordrequesterinterface.cpp
+  removecollectionrecursivetask.cpp
+  resourcestateinterface.cpp
+  resourcetask.cpp
+  retrievecollectionmetadatatask.cpp
+  retrievecollectionstask.cpp
+  retrieveitemtask.cpp
+  retrieveitemstask.cpp
+  searchtask.cpp
+  sessionpool.cpp
+  uidvalidityattribute.cpp
+  uidnextattribute.cpp
+  settings.cpp
+  subscriptiondialog.cpp
+  imapidlemanager.cpp
+  resourcestate.cpp
+  collectionmetadatahelper.cpp
+  replacemessagejob.cpp
+  tracer.cpp
+  ${AKONADI_COLLECTIONATTRIBUTES_SHARED_SOURCES}
+  ${AKONADI_IMAPATTRIBUTES_SHARED_SOURCES}
+)
+
+ecm_qt_declare_logging_category(imapresource_LIB_SRCS HEADER imapresource_debug.h IDENTIFIER IMAPRESOURCE_LOG CATEGORY_NAME log_imapresource)
+kcfg_generate_dbus_interface( ${CMAKE_CURRENT_SOURCE_DIR}/imapresource.kcfg org.kde.Akonadi.Imap.Settings )
+kconfig_add_kcfg_files(imapresource_LIB_SRCS settingsbase.kcfgc)
+
+qt5_add_dbus_adaptor( imapresource_LIB_SRCS
+     ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.Imap.Settings.xml settings.h Settings
+     )
+
+qt5_generate_dbus_interface( ${CMAKE_CURRENT_SOURCE_DIR}/imapresourcebase.h org.kde.Akonadi.Imap.Resource.xml OPTIONS -a )
+qt5_add_dbus_adaptor( imapresource_LIB_SRCS
+     ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.Imap.Resource.xml
+        imapresourcebase.h ImapResourceBase
+     )
+
+add_library(imapresource STATIC ${imapresource_LIB_SRCS})
+
+target_link_libraries(imapresource
+    Qt5::DBus
+    Qt5::Network
+    KF5::AkonadiCore
+    KF5::IMAP
+    KF5::MailTransport
+    KF5::KIOWidgets
+    KF5::Mime
+    KF5::AkonadiMime
+    KF5::IdentityManagement
+    KF5::AkonadiAgentBase
+    KF5::I18n
+    KF5::WindowSystem
+    akonadi-singlefileresource   
+)
+
+########### next target ###############
+
+set( akonadi_imap_resource_SRCS
+  main.cpp
+  imapresource.cpp
+  resourcestate.cpp
+  settingspasswordrequester.cpp
+  setupserver.cpp
+  serverinfodialog.cpp
+)
+
+install( FILES imapresource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents" )
+
+ki18n_wrap_ui(akonadi_imap_resource_SRCS setupserverview_desktop.ui)
+ki18n_wrap_ui(akonadi_imap_resource_SRCS serverinfo.ui)
+
+if (RUNTIME_PLUGINS_STATIC)
+    add_definitions(-DMAIL_SERIALIZER_PLUGIN_STATIC)
+endif ()
+
+add_executable(akonadi_imap_resource ${akonadi_imap_resource_SRCS})
+
+if( APPLE )
+  set_target_properties(akonadi_imap_resource PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../Info.plist.template)
+  set_target_properties(akonadi_imap_resource PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.Imap")
+  set_target_properties(akonadi_imap_resource PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi IMAP Resource")
+endif ()
+
+target_link_libraries(akonadi_imap_resource
+    Qt5::DBus
+    KF5::AkonadiCore
+    KF5::IMAP
+    akonadi-singlefileresource
+    KF5::AkonadiWidgets
+    KF5::MailTransport
+    KF5::Mime
+    KF5::AkonadiMime
+    KF5::IdentityManagement
+    KF5::I18n
+    imapresource
+    folderarchivesettings)
+
+if (RUNTIME_PLUGINS_STATIC)
+  target_link_libraries(akonadi_imap_resource akonadi_serializer_mail)
+endif ()
+
+install(TARGETS akonadi_imap_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
+
+add_subdirectory( wizard )
+
+if(BUILD_TESTING)
+    add_subdirectory( autotests )
+    add_subdirectory( tests )
+endif()
diff --git a/resources/imap/Messages.sh b/resources/imap/Messages.sh
new file mode 100755 (executable)
index 0000000..a1ec35a
--- /dev/null
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+$EXTRACTRC *.ui *.kcfg >> rc.cpp
+$XGETTEXT *.cpp -o $podir/akonadi_imap_resource.pot
diff --git a/resources/imap/addcollectiontask.cpp b/resources/imap/addcollectiontask.cpp
new file mode 100644 (file)
index 0000000..35b6737
--- /dev/null
@@ -0,0 +1,159 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "addcollectiontask.h"
+
+#include "collectionannotationsattribute.h"
+
+#include "imapresource_debug.h"
+#include "imapresource_debug.h"
+#include <KLocalizedString>
+
+#include <kimap/createjob.h>
+#include <kimap/session.h>
+#include <kimap/setmetadatajob.h>
+#include <kimap/subscribejob.h>
+
+#include <collectiondeletejob.h>
+
+AddCollectionTask::AddCollectionTask(const ResourceStateInterface::Ptr &resource, QObject *parent)
+    : ResourceTask(DeferIfNoSession, resource, parent), m_pendingJobs(0), m_session(Q_NULLPTR)
+{
+}
+
+AddCollectionTask::~AddCollectionTask()
+{
+}
+
+void AddCollectionTask::doStart(KIMAP::Session *session)
+{
+    if (parentCollection().remoteId().isEmpty()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Parent collection has no remote id, aborting." << collection().name() << parentCollection().name();
+        emitError(i18n("Cannot add IMAP folder '%1' for a non-existing parent folder '%2'.",
+                       collection().name(),
+                       parentCollection().name()));
+        changeProcessed();
+        return;
+    }
+
+    const QChar separator = separatorCharacter();
+    m_pendingJobs = 0;
+    m_session = session;
+    m_collection = collection();
+    m_collection.setName(m_collection.name().replace(separator, QString()));
+    m_collection.setRemoteId(separator + m_collection.name());
+
+    QString newMailBox = mailBoxForCollection(parentCollection());
+
+    if (!newMailBox.isEmpty()) {
+        newMailBox += separator;
+    }
+
+    newMailBox += m_collection.name();
+
+    qCDebug(IMAPRESOURCE_LOG) << "New folder: " << newMailBox;
+
+    KIMAP::CreateJob *job = new KIMAP::CreateJob(session);
+    job->setMailBox(newMailBox);
+
+    connect(job, &KIMAP::CreateJob::result, this, &AddCollectionTask::onCreateDone);
+
+    job->start();
+}
+
+void AddCollectionTask::onCreateDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Failed to create folder on server: " << job->errorString();
+        emitError(i18n("Failed to create the folder '%1' on the IMAP server. ",
+                       m_collection.name()));
+        cancelTask(job->errorString());
+    } else {
+        // Automatically subscribe to newly created mailbox
+        KIMAP::CreateJob *create = static_cast<KIMAP::CreateJob *>(job);
+
+        KIMAP::SubscribeJob *subscribe = new KIMAP::SubscribeJob(create->session());
+        subscribe->setMailBox(create->mailBox());
+
+        connect(subscribe, &KIMAP::SubscribeJob::result, this, &AddCollectionTask::onSubscribeDone);
+
+        subscribe->start();
+    }
+}
+
+void AddCollectionTask::onSubscribeDone(KJob *job)
+{
+    if (job->error() && isSubscriptionEnabled()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Failed to subscribe to the new folder: " << job->errorString();
+        emitWarning(i18n("Failed to subscribe to the folder '%1' on the IMAP server. "
+                         "It will disappear on next sync. Use the subscription dialog to overcome that",
+                         m_collection.name()));
+    }
+
+    const Akonadi::CollectionAnnotationsAttribute *attribute = m_collection.attribute<Akonadi::CollectionAnnotationsAttribute>();
+    if (!attribute || !serverSupportsAnnotations()) {
+        // we are finished
+        changeCommitted(m_collection);
+        synchronizeCollectionTree();
+        return;
+    }
+
+    const QMap<QByteArray, QByteArray> annotations = attribute->annotations();
+
+    foreach (const QByteArray & entry, annotations.keys()) {  //krazy:exclude=foreach
+        KIMAP::SetMetaDataJob *job = new KIMAP::SetMetaDataJob(m_session);
+        if (serverCapabilities().contains(QStringLiteral("METADATA"))) {
+            job->setServerCapability(KIMAP::MetaDataJobBase::Metadata);
+        } else {
+            job->setServerCapability(KIMAP::MetaDataJobBase::Annotatemore);
+        }
+        job->setMailBox(mailBoxForCollection(m_collection));
+
+        if (!entry.startsWith("/shared") && !entry.startsWith("/private")) {
+            //Support for legacy annotations that don't include the prefix
+            job->addMetaData(QByteArray("/shared") + entry, annotations[entry]);
+        } else {
+            job->addMetaData(entry, annotations[entry]);
+        }
+
+        connect(job, &KIMAP::SetMetaDataJob::result, this, &AddCollectionTask::onSetMetaDataDone);
+
+        m_pendingJobs++;
+
+        job->start();
+    }
+}
+
+void AddCollectionTask::onSetMetaDataDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Failed to write annotations: " << job->errorString();
+        emitWarning(i18n("Failed to write some annotations for '%1' on the IMAP server. %2",
+                         collection().name(), job->errorText()));
+    }
+
+    m_pendingJobs--;
+
+    if (m_pendingJobs == 0) {
+        changeCommitted(m_collection);
+    }
+}
+
diff --git a/resources/imap/addcollectiontask.h b/resources/imap/addcollectiontask.h
new file mode 100644 (file)
index 0000000..1227288
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef ADDCOLLECTIONTASK_H
+#define ADDCOLLECTIONTASK_H
+
+#include "resourcetask.h"
+
+namespace KIMAP
+{
+class Session;
+}
+
+class AddCollectionTask : public ResourceTask
+{
+    Q_OBJECT
+
+public:
+    explicit AddCollectionTask(const ResourceStateInterface::Ptr &resource, QObject *parent = Q_NULLPTR);
+    virtual ~AddCollectionTask();
+
+private Q_SLOTS:
+    void onCreateDone(KJob *job);
+    void onSubscribeDone(KJob *job);
+    void onSetMetaDataDone(KJob *job);
+
+protected:
+    void doStart(KIMAP::Session *session) Q_DECL_OVERRIDE;
+
+private:
+    Akonadi::Collection m_collection;
+    uint m_pendingJobs;
+    KIMAP::Session *m_session;
+};
+
+#endif
diff --git a/resources/imap/additemtask.cpp b/resources/imap/additemtask.cpp
new file mode 100644 (file)
index 0000000..082a6a1
--- /dev/null
@@ -0,0 +1,215 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "additemtask.h"
+
+#include <QtCore/QUuid>
+
+#include "imapresource_debug.h"
+#include "imapresource_debug.h"
+#include <KLocalizedString>
+
+#include <kimap/appendjob.h>
+#include <kimap/imapset.h>
+#include <kimap/searchjob.h>
+#include <kimap/selectjob.h>
+#include <kimap/session.h>
+
+#include <kmime/kmime_message.h>
+
+#include "uidnextattribute.h"
+
+AddItemTask::AddItemTask(ResourceStateInterface::Ptr resource, QObject *parent)
+    : ResourceTask(DeferIfNoSession, resource, parent)
+{
+
+}
+
+AddItemTask::~AddItemTask()
+{
+}
+
+void AddItemTask::doStart(KIMAP::Session *session)
+{
+    if (!item().hasPayload<KMime::Message::Ptr>()) {
+        changeProcessed();
+        return;
+    }
+
+    const QString mailBox = mailBoxForCollection(collection());
+    if (mailBox.isEmpty()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Trying to append message to invalid mailbox, this will fail. Id: " << parentCollection().id();
+    }
+
+    qCDebug(IMAPRESOURCE_LOG) << "Got notification about item added for local id " << item().id() << " and remote id " << item().remoteId();
+
+    // save message to the server.
+    KMime::Message::Ptr msg = item().payload<KMime::Message::Ptr>();
+    m_messageId = msg->messageID()->asUnicodeString().toUtf8();
+
+    KIMAP::AppendJob *job = new KIMAP::AppendJob(session);
+    job->setMailBox(mailBox);
+    job->setContent(msg->encodedContent(true));
+    job->setFlags(fromAkonadiToSupportedImapFlags(item().flags().toList(), collection()));
+    job->setInternalDate(msg->date()->dateTime());
+    connect(job, &KIMAP::AppendJob::result, this, &AddItemTask::onAppendMessageDone);
+    job->start();
+}
+
+void AddItemTask::onAppendMessageDone(KJob *job)
+{
+    KIMAP::AppendJob *append = qobject_cast<KIMAP::AppendJob *>(job);
+
+    if (append->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << append->errorString();
+        cancelTask(append->errorString());
+        return;
+    }
+
+    qint64 uid = append->uid();
+
+    if (uid > 0) {
+        // We got it directly if UIDPLUS is supported...
+        applyFoundUid(uid);
+
+    } else {
+        // ... otherwise prepare searching for the message
+        KIMAP::Session *session = append->session();
+        const QString mailBox = append->mailBox();
+
+        if (session->selectedMailBox() != mailBox) {
+            KIMAP::SelectJob *select = new KIMAP::SelectJob(session);
+            select->setMailBox(mailBox);
+
+            connect(select, &KJob::result,
+                    this, &AddItemTask::onPreSearchSelectDone);
+
+            select->start();
+
+        } else {
+            triggerSearchJob(session);
+        }
+    }
+}
+
+void AddItemTask::onPreSearchSelectDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << job->errorString();
+        cancelTask(job->errorString());
+    } else {
+        KIMAP::SelectJob *select = static_cast<KIMAP::SelectJob *>(job);
+        triggerSearchJob(select->session());
+    }
+}
+
+void AddItemTask::triggerSearchJob(KIMAP::Session *session)
+{
+    KIMAP::SearchJob *search = new KIMAP::SearchJob(session);
+
+    search->setUidBased(true);
+    search->setSearchLogic(KIMAP::SearchJob::And);
+
+    if (!m_messageId.isEmpty()) {
+        QByteArray header = "Message-ID ";
+        header += m_messageId;
+
+        search->addSearchCriteria(KIMAP::SearchJob::Header, header);
+    } else {
+        search->addSearchCriteria(KIMAP::SearchJob::New);
+
+        Akonadi::Collection c = collection();
+        UidNextAttribute *uidNext = c.attribute<UidNextAttribute>();
+        if (!uidNext) {
+            cancelTask(i18n("Could not determine the UID for the newly created message on the server"));
+            search->deleteLater();
+            return;
+        }
+        KIMAP::ImapInterval interval(uidNext->uidNext());
+
+        search->addSearchCriteria(KIMAP::SearchJob::Uid, interval.toImapSequence());
+    }
+
+    connect(search, &KJob::result,
+            this, &AddItemTask::onSearchDone);
+
+    search->start();
+}
+
+void AddItemTask::onSearchDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << job->errorString();
+        cancelTask(job->errorString());
+        return;
+    }
+
+    KIMAP::SearchJob *search = static_cast<KIMAP::SearchJob *>(job);
+
+    qint64 uid = 0;
+    if (search->results().count() == 1) {
+        uid = search->results().at(0);
+    }
+
+    applyFoundUid(uid);
+}
+
+void AddItemTask::applyFoundUid(qint64 uid)
+{
+    Akonadi::Item i = item();
+
+    // if we didn't manage to get a valid UID from the server, use a random RID instead
+    // this will make ItemSync clean up the mess during the next sync (while empty RIDs are protected as not yet existing on the server)
+    if (uid > 0) {
+        i.setRemoteId(QString::number(uid));
+    } else {
+        i.setRemoteId(QUuid::createUuid().toString());
+    }
+    qCDebug(IMAPRESOURCE_LOG) << "Setting remote ID to " << i.remoteId() << " for item with local id " << i.id();
+
+    changeCommitted(i);
+
+    Akonadi::Collection c = collection();
+
+    // Get the current uid next value and store it
+    UidNextAttribute *uidAttr = Q_NULLPTR;
+    int oldNextUid = 0;
+    if (c.hasAttribute("uidnext")) {
+        uidAttr = static_cast<UidNextAttribute *>(c.attribute("uidnext"));
+        oldNextUid = uidAttr->uidNext();
+    }
+
+    // If the uid we just got back is the expected next one of the box
+    // then update the property to the probable next uid to keep the cache in sync.
+    // If not something happened in our back, so we don't update and a refetch will
+    // happen at some point.
+    if (uid == oldNextUid) {
+        if (uidAttr == Q_NULLPTR) {
+            uidAttr = new UidNextAttribute(uid + 1);
+            c.addAttribute(uidAttr);
+        } else {
+            uidAttr->setUidNext(uid + 1);
+        }
+
+        applyCollectionChanges(c);
+    }
+}
+
diff --git a/resources/imap/additemtask.h b/resources/imap/additemtask.h
new file mode 100644 (file)
index 0000000..e1d96da
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef ADDITEMTASK_H
+#define ADDITEMTASK_H
+
+#include "resourcetask.h"
+
+class AddItemTask : public ResourceTask
+{
+    Q_OBJECT
+
+public:
+    explicit AddItemTask(ResourceStateInterface::Ptr resource, QObject *parent = Q_NULLPTR);
+    virtual ~AddItemTask();
+
+private Q_SLOTS:
+    void onAppendMessageDone(KJob *job);
+    void onPreSearchSelectDone(KJob *job);
+    void onSearchDone(KJob *job);
+
+protected:
+    void doStart(KIMAP::Session *session) Q_DECL_OVERRIDE;
+
+private:
+    void triggerSearchJob(KIMAP::Session *session);
+    void applyFoundUid(qint64 uid);
+
+    QByteArray m_messageId;
+};
+
+#endif
diff --git a/resources/imap/autotests/CMakeLists.txt b/resources/imap/autotests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..70dca0e
--- /dev/null
@@ -0,0 +1,51 @@
+kde_enable_exceptions()
+set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR})
+
+include(ECMMarkAsTest)
+
+find_package(Qt5Test CONFIG REQUIRED)
+
+
+# if kdepimlibs was built without -DKDE4_BUILD_TESTS, kimaptest doesn't exist.
+find_path(KIMAPTEST_INCLUDE_DIR NAMES KF5/kimaptest/fakeserver.h)
+find_library(KIMAPTEST_LIBRARY NAMES kimaptest)
+
+MESSAGE( STATUS "KIMAPTEST_INCLUDE_DIR :${KIMAPTEST_INCLUDE_DIR}")
+MESSAGE( STATUS "KIMAPTEST_LIBRARY :${KIMAPTEST_LIBRARY}")
+
+remove_definitions(-DQT_NO_CAST_FROM_ASCII)
+
+if(KIMAPTEST_INCLUDE_DIR AND KIMAPTEST_LIBRARY)
+  MACRO(IMAP_RESOURCE_UNIT_TESTS)
+    FOREACH(_testname ${ARGN})
+      include_directories(${CMAKE_CURRENT_SOURCE_DIR}/.. ${CMAKE_CURRENT_BINARY_DIR}/..)
+      add_executable( ${_testname} ${_testname}.cpp dummypasswordrequester.cpp dummyresourcestate.cpp imaptestbase.cpp )
+      add_test( ${_testname} ${_testname} )
+      ecm_mark_as_test(imap-${_testname})
+
+      target_link_libraries(${_testname} 
+            KF5::IMAP Qt5::Gui Qt5::Core ${KIMAPTEST_LIBRARY} Qt5::Test imapresource akonadi-singlefileresource)
+      add_definitions(-DTEST_DATA="\\"${CMAKE_CURRENT_SOURCE_DIR}\\"")
+    ENDFOREACH(_testname)
+  ENDMACRO(IMAP_RESOURCE_UNIT_TESTS)
+
+  IMAP_RESOURCE_UNIT_TESTS(
+    testresourcetask
+    testsessionpool
+
+    testaddcollectiontask
+    testadditemtask
+    testchangecollectiontask
+    testchangeitemtask
+    testexpungecollectiontask
+    testmovecollectiontask
+    testmoveitemstask
+    testremovecollectionrecursivetask
+    testretrievecollectionmetadatatask
+    testretrievecollectionstask
+    testretrieveitemtask
+    testretrieveitemstask
+  )
+
+endif()
+
diff --git a/resources/imap/autotests/dummypasswordrequester.cpp b/resources/imap/autotests/dummypasswordrequester.cpp
new file mode 100644 (file)
index 0000000..718a835
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "dummypasswordrequester.h"
+
+#include <QtCore/QTimer>
+
+#include <qtest.h>
+
+DummyPasswordRequester::DummyPasswordRequester(QObject *parent)
+    : PasswordRequesterInterface(parent)
+{
+    for (int i = 0; i < 10; ++i) {
+        m_expectedCalls << StandardRequest;
+        m_results << PasswordRetrieved;
+    }
+}
+
+QString DummyPasswordRequester::password() const
+{
+    return m_password;
+}
+
+void DummyPasswordRequester::setPassword(const QString &password)
+{
+    m_password = password;
+}
+
+void DummyPasswordRequester::setScenario(const QList<RequestType> &expectedCalls,
+        const QList<ResultType> &results)
+{
+    Q_ASSERT(expectedCalls.size() == results.size());
+
+    m_expectedCalls = expectedCalls;
+    m_results = results;
+}
+
+void DummyPasswordRequester::setDelays(const QList<int> &delays)
+{
+    m_delays = delays;
+}
+
+void DummyPasswordRequester::requestPassword(RequestType request,
+        const QString &/*serverError*/)
+{
+    QVERIFY2(!m_expectedCalls.isEmpty(), QString::fromLatin1("Got unexpected call: %1").arg(request).toUtf8().constData());
+    QCOMPARE((int)request, (int)m_expectedCalls.takeFirst());
+
+    int delay = 20;
+    if (!m_delays.isEmpty()) {
+        delay = m_delays.takeFirst();
+    }
+
+    QTimer::singleShot(delay, this, &DummyPasswordRequester::emitResult);
+}
+
+void DummyPasswordRequester::emitResult()
+{
+    ResultType result = m_results.takeFirst();
+
+    if (result == PasswordRetrieved) {
+        Q_EMIT done(result, m_password);
+    } else {
+        Q_EMIT done(result);
+    }
+}
+
diff --git a/resources/imap/autotests/dummypasswordrequester.h b/resources/imap/autotests/dummypasswordrequester.h
new file mode 100644 (file)
index 0000000..f629988
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef DUMMYPASSWORDREQUESTER_H
+#define DUMMYPASSWORDREQUESTER_H
+
+#include "passwordrequesterinterface.h"
+
+class DummyPasswordRequester : public PasswordRequesterInterface
+{
+    Q_OBJECT
+public:
+    DummyPasswordRequester(QObject *parent = Q_NULLPTR);
+
+    QString password() const;
+    void setPassword(const QString &password);
+
+    void setScenario(const QList<RequestType> &expectedCalls,
+                     const QList<ResultType> &results);
+    void setDelays(const QList<int> &delays);
+
+public:
+    virtual void requestPassword(RequestType request = StandardRequest,
+                                 const QString &serverError = QString());
+
+private Q_SLOTS:
+    void emitResult();
+
+private:
+    QString m_password;
+    QList<RequestType> m_expectedCalls;
+    QList<ResultType> m_results;
+    QList<int> m_delays;
+};
+
+#endif
+
diff --git a/resources/imap/autotests/dummyresourcestate.cpp b/resources/imap/autotests/dummyresourcestate.cpp
new file mode 100644 (file)
index 0000000..e04b51f
--- /dev/null
@@ -0,0 +1,442 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "dummyresourcestate.h"
+Q_DECLARE_METATYPE(QList<qint64>)
+Q_DECLARE_METATYPE(QVector<qint64>)
+Q_DECLARE_METATYPE(QString)
+Q_DECLARE_METATYPE(TagListAndMembers)
+
+DummyResourceState::DummyResourceState()
+    : m_automaticExpunge(true), m_subscriptionEnabled(true),
+      m_disconnectedMode(true), m_intervalCheckTime(-1),
+      m_mergeMode(Akonadi::ItemSync::RIDMerge)
+{
+    qRegisterMetaType<QList<qint64> >();
+    qRegisterMetaType<QVector<qint64> >();
+    qRegisterMetaType<TagListAndMembers>();
+}
+
+DummyResourceState::~DummyResourceState()
+{
+
+}
+
+void DummyResourceState::setUserName(const QString &name)
+{
+    m_userName = name;
+}
+
+QString DummyResourceState::userName() const
+{
+    return m_userName;
+}
+
+void DummyResourceState::setResourceName(const QString &name)
+{
+    m_resourceName = name;
+}
+
+QString DummyResourceState::resourceName() const
+{
+    return m_resourceName;
+}
+
+void DummyResourceState::setResourceIdentifier(const QString &identifier)
+{
+    m_resourceIdentifier = identifier;
+}
+
+QString DummyResourceState::resourceIdentifier() const
+{
+    return m_resourceIdentifier;
+}
+
+void DummyResourceState::setServerCapabilities(const QStringList &capabilities)
+{
+    m_capabilities = capabilities;
+}
+
+QStringList DummyResourceState::serverCapabilities() const
+{
+    return m_capabilities;
+}
+
+void DummyResourceState::setServerNamespaces(const QList<KIMAP::MailBoxDescriptor> &namespaces)
+{
+    m_namespaces = namespaces;
+}
+
+QList<KIMAP::MailBoxDescriptor> DummyResourceState::serverNamespaces() const
+{
+    return m_namespaces;
+}
+
+QList<KIMAP::MailBoxDescriptor> DummyResourceState::personalNamespaces() const
+{
+    return QList<KIMAP::MailBoxDescriptor>();
+}
+
+QList<KIMAP::MailBoxDescriptor> DummyResourceState::userNamespaces() const
+{
+    return QList<KIMAP::MailBoxDescriptor>();
+}
+
+QList<KIMAP::MailBoxDescriptor> DummyResourceState::sharedNamespaces() const
+{
+    return QList<KIMAP::MailBoxDescriptor>();
+}
+
+void DummyResourceState::setAutomaticExpungeEnagled(bool enabled)
+{
+    m_automaticExpunge = enabled;
+}
+
+bool DummyResourceState::isAutomaticExpungeEnabled() const
+{
+    return m_automaticExpunge;
+}
+
+void DummyResourceState::setSubscriptionEnabled(bool enabled)
+{
+    m_subscriptionEnabled = enabled;
+}
+
+bool DummyResourceState::isSubscriptionEnabled() const
+{
+    return m_subscriptionEnabled;
+}
+
+void DummyResourceState::setDisconnectedModeEnabled(bool enabled)
+{
+    m_disconnectedMode = enabled;
+}
+
+bool DummyResourceState::isDisconnectedModeEnabled() const
+{
+    return m_disconnectedMode;
+}
+
+void DummyResourceState::setIntervalCheckTime(int interval)
+{
+    m_intervalCheckTime = interval;
+}
+
+int DummyResourceState::intervalCheckTime() const
+{
+    return m_intervalCheckTime;
+}
+
+void DummyResourceState::setCollection(const Akonadi::Collection &collection)
+{
+    m_collection = collection;
+}
+
+Akonadi::Collection DummyResourceState::collection() const
+{
+    return m_collection;
+}
+
+void DummyResourceState::setItem(const Akonadi::Item &item)
+{
+    m_items.clear();
+    m_items << item;
+}
+
+Akonadi::Item DummyResourceState::item() const
+{
+    return m_items.first();
+}
+
+Akonadi::Item::List DummyResourceState::items() const
+{
+    return m_items;
+}
+
+void DummyResourceState::setParentCollection(const Akonadi::Collection &collection)
+{
+    m_parentCollection = collection;
+}
+
+Akonadi::Collection DummyResourceState::parentCollection() const
+{
+    return m_parentCollection;
+}
+
+void DummyResourceState::setSourceCollection(const Akonadi::Collection &collection)
+{
+    m_sourceCollection = collection;
+}
+
+Akonadi::Collection DummyResourceState::sourceCollection() const
+{
+    return m_sourceCollection;
+}
+
+void DummyResourceState::setTargetCollection(const Akonadi::Collection &collection)
+{
+    m_targetCollection = collection;
+}
+
+Akonadi::Collection DummyResourceState::targetCollection() const
+{
+    return m_targetCollection;
+}
+
+void DummyResourceState::setParts(const QSet<QByteArray> &parts)
+{
+    m_parts = parts;
+}
+
+QSet<QByteArray> DummyResourceState::parts() const
+{
+    return m_parts;
+}
+
+void DummyResourceState::setTag(const Akonadi::Tag &tag)
+{
+    m_tag = tag;
+}
+
+Akonadi::Tag DummyResourceState::tag() const
+{
+    return m_tag;
+}
+
+void DummyResourceState::setAddedTags(const QSet<Akonadi::Tag> &addedTags)
+{
+    m_addedTags = addedTags;
+}
+
+QSet<Akonadi::Tag> DummyResourceState::addedTags() const
+{
+    return m_addedTags;
+}
+
+void DummyResourceState::setRemovedTags(const QSet<Akonadi::Tag> &removedTags)
+{
+    m_removedTags = removedTags;
+}
+
+QSet<Akonadi::Tag> DummyResourceState::removedTags() const
+{
+    return m_removedTags;
+}
+
+Akonadi::Relation::List DummyResourceState::addedRelations() const
+{
+    return Akonadi::Relation::List();
+}
+
+Akonadi::Relation::List DummyResourceState::removedRelations() const
+{
+    return Akonadi::Relation::List();
+}
+
+QString DummyResourceState::rootRemoteId() const
+{
+    return QStringLiteral("root-id");
+}
+
+void DummyResourceState::setIdleCollection(const Akonadi::Collection &collection)
+{
+    recordCall("setIdleCollection",  QVariant::fromValue(collection));
+}
+
+void DummyResourceState::applyCollectionChanges(const Akonadi::Collection &collection)
+{
+    recordCall("applyCollectionChanges",  QVariant::fromValue(collection));
+}
+
+void DummyResourceState::collectionAttributesRetrieved(const Akonadi::Collection &collection)
+{
+    recordCall("collectionAttributesRetrieved", QVariant::fromValue(collection));
+}
+
+void DummyResourceState::itemRetrieved(const Akonadi::Item &item)
+{
+    recordCall("itemRetrieved", QVariant::fromValue(item));
+}
+
+void DummyResourceState::itemsRetrieved(const Akonadi::Item::List &items)
+{
+    recordCall("itemsRetrieved",  QVariant::fromValue(items));
+}
+
+void DummyResourceState::itemsRetrievedIncremental(const Akonadi::Item::List &changed, const Akonadi::Item::List &removed)
+{
+    Q_UNUSED(removed)
+
+    recordCall("itemsRetrievedIncremental",  QVariant::fromValue(changed));
+}
+
+void DummyResourceState::itemsRetrievalDone()
+{
+    recordCall("itemsRetrievalDone");
+}
+
+void DummyResourceState::setTotalItems(int)
+{
+
+}
+
+QSet< QByteArray > DummyResourceState::addedFlags() const
+{
+    return QSet<QByteArray>();
+}
+
+QSet< QByteArray > DummyResourceState::removedFlags() const
+{
+    return QSet<QByteArray>();
+}
+
+void DummyResourceState::itemChangeCommitted(const Akonadi::Item &item)
+{
+    recordCall("itemChangeCommitted",  QVariant::fromValue(item));
+}
+
+void DummyResourceState::itemsChangesCommitted(const Akonadi::Item::List &items)
+{
+    recordCall("itemsChangesCommitted", QVariant::fromValue(items));
+}
+
+void DummyResourceState::collectionsRetrieved(const Akonadi::Collection::List &collections)
+{
+    recordCall("collectionsRetrieved",  QVariant::fromValue(collections));
+}
+
+void DummyResourceState::collectionChangeCommitted(const Akonadi::Collection &collection)
+{
+    recordCall("collectionChangeCommitted", QVariant::fromValue(collection));
+}
+
+void DummyResourceState::tagsRetrieved(const Akonadi::Tag::List &tags, const QHash<QString, Akonadi::Item::List> &items)
+{
+    recordCall("tagsRetrieved",  QVariant::fromValue(qMakePair(tags, items)));
+}
+
+void DummyResourceState::relationsRetrieved(const Akonadi::Relation::List &relations)
+{
+    recordCall("relationsRetrieved",  QVariant::fromValue(relations));
+}
+
+void DummyResourceState::tagChangeCommitted(const Akonadi::Tag &tag)
+{
+    recordCall("tagChangeCommitted", QVariant::fromValue(tag));
+}
+
+void DummyResourceState::changeProcessed()
+{
+    recordCall("changeProcessed");
+}
+
+void DummyResourceState::searchFinished(const QVector<qint64> &result, bool isRid)
+{
+    Q_UNUSED(isRid);
+    recordCall("searchFinished", QVariant::fromValue(result));
+}
+
+void DummyResourceState::cancelTask(const QString &errorString)
+{
+    recordCall("cancelTask", QVariant::fromValue(errorString));
+}
+
+void DummyResourceState::deferTask()
+{
+    recordCall("deferTask");
+}
+
+void DummyResourceState::restartItemRetrieval(Akonadi::Collection::Id col)
+{
+    recordCall("restartItemRetrieval", QVariant::fromValue(col));
+}
+
+void DummyResourceState::taskDone()
+{
+    recordCall("taskDone");
+}
+
+void DummyResourceState::emitError(const QString &message)
+{
+    recordCall("emitError", QVariant::fromValue(message));
+}
+
+void DummyResourceState::emitWarning(const QString &message)
+{
+    recordCall("emitWarning", QVariant::fromValue(message));
+}
+
+void DummyResourceState::emitPercent(int percent)
+{
+    Q_UNUSED(percent);
+    // FIXME: Many tests need to be updated for this to be uncommented out.
+    // recordCall( "emitPercent", QVariant::fromValue(percent) );
+}
+
+void DummyResourceState::synchronizeCollectionTree()
+{
+    recordCall("synchronizeCollectionTree");
+}
+
+void DummyResourceState::scheduleConnectionAttempt()
+{
+    recordCall("scheduleConnectionAttempt");
+}
+
+void DummyResourceState::showInformationDialog(const QString &message, const QString &, const QString &)
+{
+    recordCall("showInformationDialog", QVariant::fromValue(message));
+}
+
+QList< QPair<QByteArray, QVariant> > DummyResourceState::calls() const
+{
+    return m_calls;
+}
+
+QChar DummyResourceState::separatorCharacter() const
+{
+    return m_separator;
+}
+
+void DummyResourceState::setSeparatorCharacter(const QChar &separator)
+{
+    m_separator = separator;
+}
+
+void DummyResourceState::recordCall(const QByteArray callName, const QVariant &parameter)
+{
+    m_calls << QPair<QByteArray, QVariant>(callName, parameter);
+}
+
+int DummyResourceState::batchSize() const
+{
+    return 10;
+}
+
+void DummyResourceState::setItemMergingMode(Akonadi::ItemSync::MergeMode mergeMode)
+{
+    m_mergeMode = mergeMode;
+}
+
+MessageHelper::Ptr DummyResourceState::messageHelper() const
+{
+    return MessageHelper::Ptr(new MessageHelper());
+}
diff --git a/resources/imap/autotests/dummyresourcestate.h b/resources/imap/autotests/dummyresourcestate.h
new file mode 100644 (file)
index 0000000..f7f52c0
--- /dev/null
@@ -0,0 +1,186 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef DUMMYRESOURCESTATE_H
+#define DUMMYRESOURCESTATE_H
+
+#include <QtCore/QPair>
+#include <QtCore/QVariant>
+
+#include "resourcestateinterface.h"
+
+typedef QPair<Akonadi::Tag::List, QHash<QString, Akonadi::Item::List> > TagListAndMembers;
+
+class DummyResourceState : public ResourceStateInterface
+{
+public:
+    typedef QSharedPointer<DummyResourceState> Ptr;
+
+    explicit DummyResourceState();
+    ~DummyResourceState();
+
+    void setUserName(const QString &name);
+    virtual QString userName() const;
+
+    void setResourceName(const QString &name);
+    virtual QString resourceName() const;
+
+    void setResourceIdentifier(const QString &identifier);
+    virtual QString resourceIdentifier() const;
+
+    void setServerCapabilities(const QStringList &capabilities);
+    virtual QStringList serverCapabilities() const;
+
+    void setServerNamespaces(const QList<KIMAP::MailBoxDescriptor> &namespaces);
+    virtual QList<KIMAP::MailBoxDescriptor> serverNamespaces() const;
+    virtual QList<KIMAP::MailBoxDescriptor> personalNamespaces() const;
+    virtual QList<KIMAP::MailBoxDescriptor> userNamespaces() const;
+    virtual QList<KIMAP::MailBoxDescriptor> sharedNamespaces() const;
+
+    void setAutomaticExpungeEnagled(bool enabled);
+    virtual bool isAutomaticExpungeEnabled() const;
+
+    void setSubscriptionEnabled(bool enabled);
+    virtual bool isSubscriptionEnabled() const;
+    void setDisconnectedModeEnabled(bool enabled);
+    virtual bool isDisconnectedModeEnabled() const;
+    void setIntervalCheckTime(int interval);
+    virtual int intervalCheckTime() const;
+
+    void setCollection(const Akonadi::Collection &collection);
+    virtual Akonadi::Collection collection() const;
+    void setItem(const Akonadi::Item &item);
+    virtual Akonadi::Item item() const;
+    virtual Akonadi::Item::List items() const;
+
+    void setParentCollection(const Akonadi::Collection &collection);
+    virtual Akonadi::Collection parentCollection() const;
+
+    void setSourceCollection(const Akonadi::Collection &collection);
+    virtual Akonadi::Collection sourceCollection() const;
+    void setTargetCollection(const Akonadi::Collection &collection);
+    virtual Akonadi::Collection targetCollection() const;
+
+    void setParts(const QSet<QByteArray> &parts);
+    virtual QSet<QByteArray> parts() const;
+
+    void setTag(const Akonadi::Tag &tag);
+    virtual Akonadi::Tag tag() const;
+    void setAddedTags(const QSet<Akonadi::Tag> &addedTags);
+    virtual QSet<Akonadi::Tag> addedTags() const;
+    void setRemovedTags(const QSet<Akonadi::Tag> &removedTags);
+    virtual QSet<Akonadi::Tag> removedTags() const;
+
+    virtual Akonadi::Relation::List addedRelations() const;
+    virtual Akonadi::Relation::List removedRelations() const;
+
+    virtual QString rootRemoteId() const;
+
+    virtual void setIdleCollection(const Akonadi::Collection &collection);
+    virtual void applyCollectionChanges(const Akonadi::Collection &collection);
+
+    virtual void collectionAttributesRetrieved(const Akonadi::Collection &collection);
+
+    virtual void itemRetrieved(const Akonadi::Item &item);
+
+    virtual void itemsRetrieved(const Akonadi::Item::List &items);
+    virtual void itemsRetrievedIncremental(const Akonadi::Item::List &changed, const Akonadi::Item::List &removed);
+    virtual void itemsRetrievalDone();
+
+    virtual void setTotalItems(int);
+
+    virtual QSet< QByteArray > addedFlags() const;
+    virtual QSet< QByteArray > removedFlags() const;
+
+    virtual void itemChangeCommitted(const Akonadi::Item &item);
+    virtual void itemsChangesCommitted(const Akonadi::Item::List &items);
+
+    virtual void collectionsRetrieved(const Akonadi::Collection::List &collections);
+
+    virtual void collectionChangeCommitted(const Akonadi::Collection &collection);
+
+    virtual void tagsRetrieved(const Akonadi::Tag::List &tags, const QHash<QString, Akonadi::Item::List> &);
+    virtual void relationsRetrieved(const Akonadi::Relation::List &tags);
+    virtual void tagChangeCommitted(const Akonadi::Tag &tag);
+
+    virtual void searchFinished(const QVector<qint64> &result, bool isRid = true);
+
+    virtual void changeProcessed();
+
+    virtual void cancelTask(const QString &errorString);
+    virtual void deferTask();
+    virtual void restartItemRetrieval(Akonadi::Collection::Id col);
+    virtual void taskDone();
+
+    virtual void emitError(const QString &message);
+    virtual void emitWarning(const QString &message);
+    virtual void emitPercent(int percent);
+
+    virtual void synchronizeCollectionTree();
+    virtual void scheduleConnectionAttempt();
+
+    virtual QChar separatorCharacter() const;
+    virtual void setSeparatorCharacter(const QChar &separator);
+
+    virtual void showInformationDialog(const QString &message, const QString &title, const QString &dontShowAgainName);
+
+    virtual int batchSize() const;
+    virtual void setItemMergingMode(Akonadi::ItemSync::MergeMode mergeMode);
+
+    virtual MessageHelper::Ptr messageHelper() const;
+
+    QList< QPair<QByteArray, QVariant> > calls() const;
+
+private:
+    void recordCall(const QByteArray callName, const QVariant &parameter = QVariant());
+
+    QString m_userName;
+    QString m_resourceName;
+    QString m_resourceIdentifier;
+    QStringList m_capabilities;
+    QList<KIMAP::MailBoxDescriptor> m_namespaces;
+
+    bool m_automaticExpunge;
+    bool m_subscriptionEnabled;
+    bool m_disconnectedMode;
+    int m_intervalCheckTime;
+    QChar m_separator;
+
+    Akonadi::ItemSync::MergeMode m_mergeMode;
+
+    Akonadi::Collection m_collection;
+    Akonadi::Item::List m_items;
+
+    Akonadi::Collection m_parentCollection;
+
+    Akonadi::Collection m_sourceCollection;
+    Akonadi::Collection m_targetCollection;
+
+    QSet<QByteArray> m_parts;
+
+    Akonadi::Tag m_tag;
+    QSet<Akonadi::Tag> m_addedTags;
+    QSet<Akonadi::Tag> m_removedTags;
+
+    QList< QPair<QByteArray, QVariant> > m_calls;
+};
+
+#endif
diff --git a/resources/imap/autotests/imaptestbase.cpp b/resources/imap/autotests/imaptestbase.cpp
new file mode 100644 (file)
index 0000000..409ac21
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "imaptestbase.h"
+#include <QTimer>
+#include <QSignalSpy>
+
+ImapTestBase::ImapTestBase(QObject *parent)
+    : QObject(parent)
+{
+
+}
+
+QString ImapTestBase::defaultUserName() const
+{
+    return QStringLiteral("test@kdab.com");
+}
+
+QString ImapTestBase::defaultPassword() const
+{
+    return QStringLiteral("foobar");
+}
+
+ImapAccount *ImapTestBase::createDefaultAccount() const
+{
+    ImapAccount *account = new ImapAccount;
+
+    account->setServer(QStringLiteral("127.0.0.1"));
+    account->setPort(5989);
+    account->setUserName(defaultUserName());
+    account->setSubscriptionEnabled(true);
+    account->setEncryptionMode(KIMAP::LoginJob::Unencrypted);
+    account->setAuthenticationMode(KIMAP::LoginJob::ClearText);
+
+    return account;
+}
+
+DummyPasswordRequester *ImapTestBase::createDefaultRequester()
+{
+    DummyPasswordRequester *requester = new DummyPasswordRequester(this);
+    requester->setPassword(defaultPassword());
+    return requester;
+}
+
+void ImapTestBase::setupTestCase()
+{
+    qRegisterMetaType<ImapAccount *>();
+    qRegisterMetaType<DummyPasswordRequester *>();
+    qRegisterMetaType<DummyResourceState::Ptr>();
+    qRegisterMetaType<KIMAP::Session *>();
+}
+
+QList<QByteArray> ImapTestBase::defaultAuthScenario() const
+{
+    QList<QByteArray> scenario;
+
+    scenario << FakeServer::greeting()
+             << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""
+             << "S: A000001 OK User Logged in";
+
+    return scenario;
+}
+
+QList<QByteArray> ImapTestBase::defaultPoolConnectionScenario(const QList<QByteArray> &customCapabilities) const
+{
+    QList<QByteArray> scenario;
+
+    QByteArray caps = "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE";
+    Q_FOREACH (const QByteArray &cap, customCapabilities) {
+        caps += " " + cap;
+    }
+
+    scenario << defaultAuthScenario()
+             << "C: A000002 CAPABILITY"
+             << caps
+             << "S: A000002 OK Completed";
+
+    return scenario;
+}
+
+bool ImapTestBase::waitForSignal(QObject *obj, const char *member, int timeout) const
+{
+    QEventLoop loop;
+    QTimer timer;
+
+    connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
+
+    QSignalSpy spy(obj, member);
+    connect(obj, member, &loop, SLOT(quit()));
+
+    timer.setSingleShot(true);
+    timer.start(timeout);
+    loop.exec();
+    timer.stop();
+
+    return spy.count() == 1;
+}
+
+Akonadi::Collection ImapTestBase::createCollectionChain(const QString &remoteId) const
+{
+    QChar separator = remoteId.length() > 0 ? remoteId.at(0) : QLatin1Char('/');
+
+    Akonadi::Collection parent(1);
+    parent.setRemoteId(QStringLiteral("root-id"));
+    parent.setParentCollection(Akonadi::Collection::root());
+    Akonadi::Collection::Id id = 2;
+
+    Akonadi::Collection collection = parent;
+
+    const QStringList collections = remoteId.split(separator, QString::SkipEmptyParts);
+    Q_FOREACH (const QString &colId, collections) {
+        collection = Akonadi::Collection(id);
+        collection.setRemoteId(separator + colId);
+        collection.setParentCollection(parent);
+
+        parent = collection;
+        id++;
+    }
+
+    return collection;
+}
+
diff --git a/resources/imap/autotests/imaptestbase.h b/resources/imap/autotests/imaptestbase.h
new file mode 100644 (file)
index 0000000..fc8a172
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef IMAPTESTBASE_H
+#define IMAPTESTBASE_H
+
+#include <qtest.h>
+
+#include <kimaptest/fakeserver.h>
+
+#include "dummypasswordrequester.h"
+#include "dummyresourcestate.h"
+#include "imapaccount.h"
+#include "resourcetask.h"
+#include "sessionpool.h"
+
+Q_DECLARE_METATYPE(ImapAccount *)
+Q_DECLARE_METATYPE(DummyPasswordRequester *)
+Q_DECLARE_METATYPE(DummyResourceState::Ptr)
+Q_DECLARE_METATYPE(KIMAP::Session *)
+Q_DECLARE_METATYPE(QVariant)
+
+class ImapTestBase : public QObject
+{
+    Q_OBJECT
+
+public:
+    ImapTestBase(QObject *parent = Q_NULLPTR);
+
+protected:
+    QString defaultUserName() const;
+    QString defaultPassword() const;
+    ImapAccount *createDefaultAccount() const;
+    DummyPasswordRequester *createDefaultRequester();
+    QList<QByteArray> defaultAuthScenario() const;
+    QList<QByteArray> defaultPoolConnectionScenario(const QList<QByteArray> &customCapabilities = QList<QByteArray>()) const;
+
+    bool waitForSignal(QObject *obj, const char *member, int timeout = 500) const;
+
+    Akonadi::Collection createCollectionChain(const QString &remoteId) const;
+
+private Q_SLOTS:
+    void setupTestCase();
+};
+
+#endif
diff --git a/resources/imap/autotests/testaddcollectiontask.cpp b/resources/imap/autotests/testaddcollectiontask.cpp
new file mode 100644 (file)
index 0000000..9b2eef3
--- /dev/null
@@ -0,0 +1,182 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "imaptestbase.h"
+
+#include "addcollectiontask.h"
+#include <collectionannotationsattribute.h>
+#include <QDebug>
+
+class TestAddCollectionTask : public ImapTestBase
+{
+    Q_OBJECT
+
+private Q_SLOTS:
+    void shouldCreateAndSubscribe_data()
+    {
+        QTest::addColumn<Akonadi::Collection>("parentCollection");
+        QTest::addColumn<Akonadi::Collection>("collection");
+        QTest::addColumn< QList<QByteArray> >("scenario");
+        QTest::addColumn<QStringList>("callNames");
+        QTest::addColumn<QString>("collectionName");
+        QTest::addColumn<QString>("remoteId");
+
+        Akonadi::Collection parentCollection;
+        Akonadi::Collection collection;
+        QList<QByteArray> scenario;
+        QStringList callNames;
+
+        parentCollection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection = Akonadi::Collection(4);
+        collection.setName(QStringLiteral("Bar"));
+        collection.setParentCollection(parentCollection);
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 CREATE \"INBOX/Foo/Bar\""
+                 << "S: A000003 OK create done"
+                 << "C: A000004 SUBSCRIBE \"INBOX/Foo/Bar\""
+                 << "S: A000004 OK subscribe done";
+
+        callNames.clear();
+        callNames << QStringLiteral("collectionChangeCommitted") << QStringLiteral("synchronizeCollectionTree");
+
+        QTest::newRow("trivial case") << parentCollection << collection << scenario << callNames
+                                      << collection.name() << "/Bar";
+
+        parentCollection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection = Akonadi::Collection(4);
+        collection.setName(QStringLiteral("Bar/Baz"));
+        collection.setParentCollection(parentCollection);
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 CREATE \"INBOX/Foo/BarBaz\""
+                 << "S: A000003 OK create done"
+                 << "C: A000004 SUBSCRIBE \"INBOX/Foo/BarBaz\""
+                 << "S: A000004 OK subscribe done";
+
+        callNames.clear();
+        callNames << QStringLiteral("collectionChangeCommitted") << QStringLiteral("synchronizeCollectionTree");
+
+        QTest::newRow("folder with invalid separator") << parentCollection << collection << scenario
+                << callNames << "BarBaz" << "/BarBaz";
+
+        parentCollection = createCollectionChain(QStringLiteral(".INBOX"));
+        collection = Akonadi::Collection(3);
+        collection.setName(QStringLiteral("Foo"));
+        collection.setParentCollection(parentCollection);
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 CREATE \"INBOX.Foo\""
+                 << "S: A000003 OK create done"
+                 << "C: A000004 SUBSCRIBE \"INBOX.Foo\""
+                 << "S: A000004 OK subscribe done";
+        callNames.clear();
+        callNames << QStringLiteral("collectionChangeCommitted") << QStringLiteral("synchronizeCollectionTree");
+
+        QTest::newRow("folder with non-standard separator") << parentCollection << collection << scenario
+                << callNames << "Foo" << ".Foo";
+
+        parentCollection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection = Akonadi::Collection(4);
+        collection.setName(QStringLiteral("Bar"));
+        collection.setParentCollection(parentCollection);
+        Akonadi::CollectionAnnotationsAttribute *attr = collection.attribute<Akonadi::CollectionAnnotationsAttribute>(Akonadi::Collection::AddIfMissing);
+        QMap<QByteArray, QByteArray> annotations;
+        annotations.insert("/shared/vendor/foobar/foo", "value");
+        attr->setAnnotations(annotations);
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 CREATE \"INBOX/Foo/Bar\""
+                 << "S: A000003 OK create done"
+                 << "C: A000004 SUBSCRIBE \"INBOX/Foo/Bar\""
+                 << "S: A000004 OK subscribe done"
+                 << "C: A000005 SETMETADATA \"INBOX/Foo/Bar\" (\"/shared/vendor/foobar/foo\" \"value\")"
+                 << "S: A000005 OK SETMETADATA complete";
+
+        callNames.clear();
+        callNames << QStringLiteral("collectionChangeCommitted");
+
+        QTest::newRow("folder with annotations") << parentCollection << collection << scenario << callNames
+                << collection.name() << "/Bar";
+    }
+
+    void shouldCreateAndSubscribe()
+    {
+        QFETCH(Akonadi::Collection, parentCollection);
+        QFETCH(Akonadi::Collection, collection);
+        QFETCH(QList<QByteArray>, scenario);
+        QFETCH(QStringList, callNames);
+        QFETCH(QString, collectionName);
+        QFETCH(QString, remoteId);
+
+        FakeServer server;
+        server.setScenario(scenario);
+        server.startAndWait();
+
+        SessionPool pool(1);
+
+        pool.setPasswordRequester(createDefaultRequester());
+        QVERIFY(pool.connect(createDefaultAccount()));
+        QVERIFY(waitForSignal(&pool, SIGNAL(connectDone(int,QString))));
+
+        DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState);
+        state->setParentCollection(parentCollection);
+        state->setCollection(collection);
+        if (collection.hasAttribute<Akonadi::CollectionAnnotationsAttribute>()) {
+            state->setServerCapabilities(QStringList() << QStringLiteral("METADATA"));
+        }
+        AddCollectionTask *task = new AddCollectionTask(state);
+        task->start(&pool);
+
+        QTRY_COMPARE(state->calls().count(), callNames.size());
+        for (int i = 0; i < callNames.size(); i++) {
+            QString command = QString::fromUtf8(state->calls().at(i).first);
+            QVariant parameter = state->calls().at(i).second;
+
+            if (command == QLatin1String("cancelTask") && callNames[i] != QLatin1String("cancelTask")) {
+                qDebug() << "Got a cancel:" << parameter.toString();
+            }
+
+            if (command == QLatin1String("collectionChangeCommitted")) {
+                QCOMPARE(parameter.value<Akonadi::Collection>().name(), collectionName);
+                QCOMPARE(parameter.value<Akonadi::Collection>().remoteId().right(collectionName.length()),
+                         collectionName);
+                QCOMPARE(parameter.value<Akonadi::Collection>().remoteId(), remoteId);
+            }
+
+            QCOMPARE(command, callNames[i]);
+
+            if (command == QLatin1String("cancelTask")) {
+                QVERIFY(!parameter.toString().isEmpty());
+            }
+        }
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+};
+
+QTEST_GUILESS_MAIN(TestAddCollectionTask)
+
+#include "testaddcollectiontask.moc"
diff --git a/resources/imap/autotests/testadditemtask.cpp b/resources/imap/autotests/testadditemtask.cpp
new file mode 100644 (file)
index 0000000..b108793
--- /dev/null
@@ -0,0 +1,179 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "imaptestbase.h"
+
+#include "additemtask.h"
+
+#include "uidnextattribute.h"
+
+#include <kmime/kmime_message.h>
+
+class TestAddItemTask : public ImapTestBase
+{
+    Q_OBJECT
+
+private Q_SLOTS:
+    void shouldAppendMessage_data()
+    {
+        QTest::addColumn<Akonadi::Item>("item");
+        QTest::addColumn<Akonadi::Collection>("collection");
+        QTest::addColumn< QList<QByteArray> >("scenario");
+        QTest::addColumn<QStringList>("callNames");
+
+        Akonadi::Collection collection;
+        Akonadi::Item item;
+        QString messageContent;
+        QList<QByteArray> scenario;
+        QStringList callNames;
+
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        UidNextAttribute *uidNext = new UidNextAttribute;
+        uidNext->setUidNext(63);
+        collection.addAttribute(uidNext);
+
+        item = Akonadi::Item(2);
+        item.setParentCollection(collection);
+
+        KMime::Message::Ptr message(new KMime::Message);
+
+        messageContent = QStringLiteral("From: ervin\nTo: someone\nSubject: foo\n\nSpeechless...");
+
+        message->setContent(messageContent.toUtf8());
+        message->parse();
+        item.setPayload(message);
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 APPEND \"INBOX/Foo\" {55}\r\n" + message->encodedContent(true)
+                 << "S: A000003 OK append done [ APPENDUID 1239890035 66 ]";
+
+        callNames.clear();
+        callNames << QStringLiteral("itemChangeCommitted");
+
+        QTest::newRow("trivial case") << item << collection << scenario << callNames;
+
+        message = KMime::Message::Ptr(new KMime::Message);
+
+        messageContent = QStringLiteral("From: ervin\nTo: someone\nSubject: foo\nMessage-ID: <42.4242.foo@bar.org>\n\nSpeechless...");
+
+        message->setContent(messageContent.toUtf8());
+        message->parse();
+        item.setPayload(message);
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 APPEND \"INBOX/Foo\" {90}\r\n" + message->encodedContent(true)
+                 << "S: A000003 OK append done"
+                 << "C: A000004 SELECT \"INBOX/Foo\""
+                 << "S: A000004 OK select done"
+                 << "C: A000005 UID SEARCH HEADER Message-ID <42.4242.foo@bar.org>"
+                 << "S: * SEARCH 66"
+                 << "S: A000005 OK search done";
+
+        callNames.clear();
+        callNames << QStringLiteral("itemChangeCommitted");
+
+        QTest::newRow("no APPENDUID, message contained Message-ID") << item << collection << scenario << callNames;
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 APPEND \"INBOX/Foo\" {90}\r\n" + message->encodedContent(true)
+                 << "S: A000003 OK append done"
+                 << "C: A000004 SELECT \"INBOX/Foo\""
+                 << "S: A000004 OK select done"
+                 << "C: A000005 UID SEARCH HEADER Message-ID <42.4242.foo@bar.org>"
+                 << "S: * SEARCH 65 66"
+                 << "S: A000005 OK search done";
+        callNames.clear();
+        callNames << QStringLiteral("itemChangeCommitted");
+        QTest::newRow("no APPENDUID, message contained non-unique Message-ID") << item << collection << scenario << callNames;
+
+        message = KMime::Message::Ptr(new KMime::Message);
+
+        messageContent = QStringLiteral("From: ervin\nTo: someone\nSubject: foo\n\nSpeechless...");
+
+        message->setContent(messageContent.toUtf8());
+        message->parse();
+        item.setPayload(message);
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 APPEND \"INBOX/Foo\" {55}\r\n" + message->encodedContent(true)
+                 << "S: A000003 OK append done"
+                 << "C: A000004 SELECT \"INBOX/Foo\""
+                 << "S: A000004 OK select done"
+                 << "C: A000005 UID SEARCH NEW UID 63:*"
+                 << "S: * SEARCH 66"
+                 << "S: A000005 OK search done";
+
+        callNames.clear();
+        callNames << QStringLiteral("itemChangeCommitted");
+
+        QTest::newRow("no APPENDUID, message didn't contain Message-ID") << item << collection << scenario << callNames;
+    }
+
+    void shouldAppendMessage()
+    {
+        QFETCH(Akonadi::Item, item);
+        QFETCH(Akonadi::Collection, collection);
+        QFETCH(QList<QByteArray>, scenario);
+        QFETCH(QStringList, callNames);
+
+        FakeServer server;
+        server.setScenario(scenario);
+        server.startAndWait();
+
+        SessionPool pool(1);
+
+        pool.setPasswordRequester(createDefaultRequester());
+        QVERIFY(pool.connect(createDefaultAccount()));
+        QVERIFY(waitForSignal(&pool, SIGNAL(connectDone(int,QString))));
+
+        DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState);
+        state->setCollection(collection);
+        state->setItem(item);
+        AddItemTask *task = new AddItemTask(state);
+        task->start(&pool);
+
+        QTRY_COMPARE(state->calls().count(), callNames.size());
+        for (int i = 0; i < callNames.size(); i++) {
+            QString command = QString::fromUtf8(state->calls().at(i).first);
+            QVariant parameter = state->calls().at(i).second;
+
+            if (command == QLatin1String("cancelTask") && callNames[i] != QLatin1String("cancelTask")) {
+                qDebug() << "Got a cancel:" << parameter.toString();
+            }
+
+            QCOMPARE(command, callNames[i]);
+
+            if (command == QLatin1String("cancelTask")) {
+                QVERIFY(!parameter.toString().isEmpty());
+            }
+        }
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+};
+
+QTEST_GUILESS_MAIN(TestAddItemTask)
+
+#include "testadditemtask.moc"
diff --git a/resources/imap/autotests/testchangecollectiontask.cpp b/resources/imap/autotests/testchangecollectiontask.cpp
new file mode 100644 (file)
index 0000000..51a826e
--- /dev/null
@@ -0,0 +1,244 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "imaptestbase.h"
+
+#include "changecollectiontask.h"
+
+#include "collectionannotationsattribute.h"
+#include "imapaclattribute.h"
+
+Q_DECLARE_METATYPE(QSet<QByteArray>)
+
+class TestChangeCollectionTask : public ImapTestBase
+{
+    Q_OBJECT
+
+private Q_SLOTS:
+    void shouldUpdateMetadataAclAndName_data()
+    {
+        QTest::addColumn<Akonadi::Collection>("collection");
+        QTest::addColumn< QSet<QByteArray> >("parts");
+        QTest::addColumn< QList<QByteArray> >("scenario");
+        QTest::addColumn<QStringList>("callNames");
+        QTest::addColumn<QString>("collectionName");
+        QTest::addColumn<QStringList>("caps");
+
+        Akonadi::Collection collection;
+        QSet<QByteArray> parts;
+        QList<QByteArray> scenario;
+        QStringList callNames;
+        QStringList caps;
+
+        collection = createCollectionChain(QStringLiteral("/Foo"));
+        collection.setName(QStringLiteral("Bar"));
+        collection.setRights(Akonadi::Collection::AllRights);
+
+        Akonadi::ImapAclAttribute *acls = new Akonadi::ImapAclAttribute;
+        QMap<QByteArray, KIMAP::Acl::Rights> rights;
+        // Old rights
+        rights["test@kdab.com"] = KIMAP::Acl::rightsFromString("lrswipckxtda");
+        rights["foo@kde.org"] = KIMAP::Acl::rightsFromString("lrswipcda");
+        acls->setRights(rights);
+
+        // New rights
+        rights["test@kdab.com"] = KIMAP::Acl::rightsFromString("lrswipckxtda");
+        rights["foo@kde.org"] = KIMAP::Acl::rightsFromString("lrswipcda");
+        acls->setRights(rights);
+        collection.addAttribute(acls);
+
+        Akonadi::CollectionAnnotationsAttribute *annotationsAttr = new Akonadi::CollectionAnnotationsAttribute;
+        QMap<QByteArray, QByteArray> annotations;
+        annotations["/vendor/kolab/folder-test"] = "false";
+        annotations["/vendor/kolab/folder-test2"] = "true";
+        annotationsAttr->setAnnotations(annotations);
+        collection.addAttribute(annotationsAttr);
+
+        parts << "NAME" << "AccessRights" << "imapacl" << "collectionannotations";
+
+        caps << QStringLiteral("ACL") << QStringLiteral("ANNOTATEMORE");
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SETACL \"Foo\" \"test@kdab.com\" \"lrswipckxtda\""
+                 << "S: A000003 OK acl changed"
+                 << "C: A000004 SETANNOTATION \"Foo\" \"/vendor/kolab/folder-test\" (\"value.shared\" \"false\")"
+                 << "S: A000004 OK annotations changed"
+                 << "C: A000005 SETANNOTATION \"Foo\" \"/vendor/kolab/folder-test2\" (\"value.shared\" \"true\")"
+                 << "S: A000005 OK annotations changed"
+                 << "C: A000006 SETACL \"Foo\" \"foo@kde.org\" \"lrswipcda\""
+                 << "S: A000006 OK acl changed"
+                 << "C: A000007 SETACL \"Foo\" \"test@kdab.com\" \"lrswipckxtda\""
+                 << "S: A000007 OK acl changed"
+                 << "C: A000008 RENAME \"Foo\" \"Bar\""
+                 << "S: A000008 OK rename done"
+                 << "C: A000009 SUBSCRIBE \"Bar\""
+                 << "S: A000009 OK mailbox subscribed";
+
+        callNames.clear();
+        callNames << QStringLiteral("collectionChangeCommitted");
+
+        QTest::newRow("complete case") << collection << parts << scenario << callNames << collection.name() << caps;
+
+        caps.clear();
+        caps << QStringLiteral("ACL");
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SETACL \"Foo\" \"test@kdab.com\" \"lrswipckxtda\""
+                 << "S: A000003 OK acl changed"
+                 << "C: A000004 SETACL \"Foo\" \"foo@kde.org\" \"lrswipcda\""
+                 << "S: A000004 OK acl changed"
+                 << "C: A000005 SETACL \"Foo\" \"test@kdab.com\" \"lrswipckxtda\""
+                 << "S: A000005 OK acl changed"
+                 << "C: A000006 RENAME \"Foo\" \"Bar\""
+                 << "S: A000006 OK rename done"
+                 << "C: A000007 SUBSCRIBE \"Bar\""
+                 << "S: A000007 OK mailbox subscribed";
+        QTest::newRow("no ANNOTATEMORE support") << collection << parts << scenario << callNames << collection.name() << caps;
+
+        collection = createCollectionChain(QStringLiteral("/Foo"));
+        collection.setName(QStringLiteral("Bar/Baz"));
+        caps.clear();
+        caps << QStringLiteral("ACL") << QStringLiteral("ANNOTATEMORE");
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 RENAME \"Foo\" \"BarBaz\""
+                 << "S: A000003 OK rename done"
+                 << "C: A000004 SUBSCRIBE \"BarBaz\""
+                 << "S: A000004 OK mailbox subscribed";
+        parts.clear();
+        parts << "NAME";
+        callNames.clear();
+        callNames << QStringLiteral("collectionChangeCommitted");
+        QTest::newRow("rename with invalid separator") << collection << parts << scenario << callNames
+                << "BarBaz" << caps;
+
+        collection = createCollectionChain(QStringLiteral(".INBOX.Foo"));
+        collection.setName(QStringLiteral("Bar"));
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 RENAME \"INBOX.Foo\" \"INBOX.Bar\""
+                 << "S: A000003 OK rename done"
+                 << "C: A000004 SUBSCRIBE \"INBOX.Bar\""
+                 << "S: A000004 OK mailbox subscribed";
+        QTest::newRow("rename with non-standard separator") << collection << parts << scenario << callNames
+                << "Bar" << caps;
+
+        collection = createCollectionChain(QStringLiteral("/Foo"));
+        collection.setName(QStringLiteral("Bar"));
+        collection.setRights(Akonadi::Collection::AllRights);
+
+        acls = new Akonadi::ImapAclAttribute;
+        // Old rights
+        rights["test@kdab.com"] = KIMAP::Acl::rightsFromString("lrswipckxtda");
+        rights["foo@kde.org"] = KIMAP::Acl::rightsFromString("lrswipcda");
+        acls->setRights(rights);
+
+        // New rights
+        rights["test@kdab.com"] = KIMAP::Acl::rightsFromString("lrswipckxtda");
+        rights["foo@kde.org"] = KIMAP::Acl::rightsFromString("lrswipcda");
+        acls->setRights(rights);
+        collection.addAttribute(acls);
+
+        annotationsAttr = new Akonadi::CollectionAnnotationsAttribute;
+        annotations["/vendor/kolab/folder-test"] = "false";
+        annotations["/vendor/kolab/folder-test2"] = "true";
+        annotationsAttr->setAnnotations(annotations);
+        collection.addAttribute(annotationsAttr);
+
+        parts << "NAME" << "AccessRights" << "imapacl" << "collectionannotations";
+
+        caps << QStringLiteral("ACL") << QStringLiteral("METADATA");
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SETACL \"Foo\" \"test@kdab.com\" \"lrswipckxtda\""
+                 << "S: A000003 OK acl changed"
+                 << "C: A000004 SETMETADATA \"Foo\" (\"/shared/vendor/kolab/folder-test\" \"false\")"
+                 << "S: A000004 OK SETMETADATA complete"
+                 << "C: A000005 SETMETADATA \"Foo\" (\"/shared/vendor/kolab/folder-test2\" \"true\")"
+                 << "S: A000005 OK SETMETADATA complete"
+                 << "C: A000006 SETACL \"Foo\" \"foo@kde.org\" \"lrswipcda\""
+                 << "S: A000006 OK acl changed"
+                 << "C: A000007 SETACL \"Foo\" \"test@kdab.com\" \"lrswipckxtda\""
+                 << "S: A000007 OK acl changed"
+                 << "C: A000008 RENAME \"Foo\" \"Bar\""
+                 << "S: A000008 OK rename done"
+                 << "C: A000009 SUBSCRIBE \"Bar\""
+                 << "S: A000009 OK mailbox subscribed";
+        QTest::newRow("complete case METADATA") << collection << parts << scenario << callNames << collection.name() << caps;
+    }
+
+    void shouldUpdateMetadataAclAndName()
+    {
+        QFETCH(Akonadi::Collection, collection);
+        QFETCH(QSet<QByteArray>, parts);
+        QFETCH(QList<QByteArray>, scenario);
+        QFETCH(QStringList, callNames);
+        QFETCH(QString, collectionName);
+        QFETCH(QStringList, caps);
+
+        FakeServer server;
+        server.setScenario(scenario);
+        server.startAndWait();
+
+        SessionPool pool(1);
+
+        pool.setPasswordRequester(createDefaultRequester());
+        QVERIFY(pool.connect(createDefaultAccount()));
+        QVERIFY(waitForSignal(&pool, SIGNAL(connectDone(int,QString))));
+
+        DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState);
+        state->setUserName(defaultUserName());
+        state->setServerCapabilities(caps);
+        state->setCollection(collection);
+        state->setParts(parts);
+        ChangeCollectionTask *task = new ChangeCollectionTask(state);
+        task->start(&pool);
+
+        QTRY_COMPARE(state->calls().count(), callNames.size());
+        for (int i = 0; i < callNames.size(); i++) {
+            QString command = QString::fromUtf8(state->calls().at(i).first);
+            QVariant parameter = state->calls().at(i).second;
+
+            if (command == QLatin1String("cancelTask") && callNames[i] != QLatin1String("cancelTask")) {
+                qDebug() << "Got a cancel:" << parameter.toString();
+            }
+
+            QCOMPARE(command, callNames[i]);
+
+            if (command == QLatin1String("cancelTask")) {
+                QVERIFY(!parameter.toString().isEmpty());
+            }
+            if (command == QLatin1String("collectionChangeCommitted")) {
+                QCOMPARE(parameter.value<Akonadi::Collection>().name(), collectionName);
+                QCOMPARE(parameter.value<Akonadi::Collection>().remoteId().right(collectionName.length()),
+                         collectionName);
+            }
+        }
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+};
+
+QTEST_GUILESS_MAIN(TestChangeCollectionTask)
+
+#include "testchangecollectiontask.moc"
diff --git a/resources/imap/autotests/testchangeitemtask.cpp b/resources/imap/autotests/testchangeitemtask.cpp
new file mode 100644 (file)
index 0000000..d219854
--- /dev/null
@@ -0,0 +1,214 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "imaptestbase.h"
+
+#include "changeitemtask.h"
+#include "uidnextattribute.h"
+
+#include <kmime/kmime_message.h>
+
+Q_DECLARE_METATYPE(QSet<QByteArray>)
+
+class TestChangeItemTask : public ImapTestBase
+{
+    Q_OBJECT
+
+private Q_SLOTS:
+    void shouldAppendMessage_data()
+    {
+        QTest::addColumn<Akonadi::Item>("item");
+        QTest::addColumn< QSet<QByteArray> >("parts");
+        QTest::addColumn< QList<QByteArray> >("scenario");
+        QTest::addColumn<QStringList>("callNames");
+
+        Akonadi::Collection collection;
+        Akonadi::Item item;
+        QSet<QByteArray> parts;
+        QString messageContent;
+        QList<QByteArray> scenario;
+        QStringList callNames;
+
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection.addAttribute(new UidNextAttribute(65));
+        item = Akonadi::Item(2);
+        item.setParentCollection(collection);
+        item.setRemoteId(QStringLiteral("5"));
+
+        KMime::Message::Ptr message(new KMime::Message);
+
+        messageContent = QStringLiteral("From: ervin\nTo: someone\nSubject: foo\n\nSpeechless...");
+
+        message->setContent(messageContent.toUtf8());
+        message->parse();
+        item.setPayload(message);
+
+        parts.clear();
+        parts << "PLD:RFC822";
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 APPEND \"INBOX/Foo\" {55}\r\n" + message->encodedContent(true)
+                 << "S: A000003 OK append done [ APPENDUID 1239890035 65 ]"
+                 << "C: A000004 SELECT \"INBOX/Foo\""
+                 << "S: A000004 OK select done"
+                 << "C: A000005 UID STORE 5 +FLAGS (\\Deleted)"
+                 << "S: A000005 OK store done";
+
+        callNames.clear();
+        callNames << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemChangeCommitted");
+
+        QTest::newRow("modifying mail content") << item << parts << scenario << callNames;
+
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection.addAttribute(new UidNextAttribute(65));
+        item = Akonadi::Item(2);
+        item.setParentCollection(collection);
+        item.setRemoteId(QStringLiteral("5"));
+
+        message = KMime::Message::Ptr(new KMime::Message);
+
+        messageContent = QStringLiteral("From: ervin\nTo: someone\nSubject: foo\nMessage-ID: <42.4242.foo@bar.org>\n\nSpeechless...");
+
+        message->setContent(messageContent.toUtf8());
+        message->parse();
+        item.setPayload(message);
+
+        parts.clear();
+        parts << "PLD:RFC822";
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 APPEND \"INBOX/Foo\" {90}\r\n" + message->encodedContent(true)
+                 << "S: A000003 OK append done"
+                 << "C: A000004 SELECT \"INBOX/Foo\""
+                 << "S: A000004 OK select done"
+                 << "C: A000005 UID SEARCH HEADER Message-ID <42.4242.foo@bar.org>"
+                 << "S: * SEARCH 65"
+                 << "S: A000005 OK search done"
+                 << "C: A000006 UID STORE 5 +FLAGS (\\Deleted)"
+                 << "S: A000006 OK store done";
+
+        callNames.clear();
+        callNames << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemChangeCommitted");
+
+        QTest::newRow("modifying mail content, no APPENDUID, message has Message-ID") << item << parts << scenario << callNames;
+
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection.addAttribute(new UidNextAttribute(65));
+        item = Akonadi::Item(2);
+        item.setParentCollection(collection);
+        item.setRemoteId(QStringLiteral("5"));
+
+        message = KMime::Message::Ptr(new KMime::Message);
+
+        messageContent = QStringLiteral("From: ervin\nTo: someone\nSubject: foo\n\nSpeechless...");
+
+        message->setContent(messageContent.toUtf8());
+        message->parse();
+        item.setPayload(message);
+
+        parts.clear();
+        parts << "PLD:RFC822";
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 APPEND \"INBOX/Foo\" {55}\r\n" + message->encodedContent(true)
+                 << "S: A000003 OK append done"
+                 << "C: A000004 SELECT \"INBOX/Foo\""
+                 << "S: A000004 OK select done"
+                 << "C: A000005 UID SEARCH NEW UID 65:*"
+                 << "S: * SEARCH 65"
+                 << "S: A000005 OK search done"
+                 << "C: A000006 UID STORE 5 +FLAGS (\\Deleted)"
+                 << "S: A000006 OK store done";
+
+        callNames.clear();
+        callNames << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemChangeCommitted");
+
+        QTest::newRow("modifying mail content, no APPENDUID, message has no Message-ID") << item << parts << scenario << callNames;
+
+        // collection unchanged for this test
+        // item only gets a set of flags
+        item.setFlags(QSet<QByteArray>() << "\\Foo");
+
+        parts.clear();
+        parts << "FLAGS";
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 OK select done"
+                 << "C: A000004 UID STORE 5 FLAGS (\\Foo)"
+                 << "S: A000004 OK store done";
+
+        callNames.clear();
+        callNames << QStringLiteral("changeProcessed");
+
+        QTest::newRow("modifying mail flags") << item << parts << scenario << callNames;
+    }
+
+    void shouldAppendMessage()
+    {
+        QFETCH(Akonadi::Item, item);
+        QFETCH(QSet<QByteArray>, parts);
+        QFETCH(QList<QByteArray>, scenario);
+        QFETCH(QStringList, callNames);
+
+        FakeServer server;
+        server.setScenario(scenario);
+        server.startAndWait();
+
+        SessionPool pool(1);
+
+        pool.setPasswordRequester(createDefaultRequester());
+        QVERIFY(pool.connect(createDefaultAccount()));
+        QVERIFY(waitForSignal(&pool, SIGNAL(connectDone(int,QString))));
+
+        DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState);
+        state->setParts(parts);
+        state->setItem(item);
+        ChangeItemTask *task = new ChangeItemTask(state);
+        task->start(&pool);
+
+        QTRY_COMPARE(state->calls().count(), callNames.size());
+        for (int i = 0; i < callNames.size(); i++) {
+            QString command = QString::fromUtf8(state->calls().at(i).first);
+            QVariant parameter = state->calls().at(i).second;
+
+            if (command == QLatin1String("cancelTask") && callNames[i] != QLatin1String("cancelTask")) {
+                qDebug() << "Got a cancel:" << parameter.toString();
+            }
+
+            QCOMPARE(command, callNames[i]);
+
+            if (command == QLatin1String("cancelTask")) {
+                QVERIFY(!parameter.toString().isEmpty());
+            }
+        }
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+};
+
+QTEST_GUILESS_MAIN(TestChangeItemTask)
+
+#include "testchangeitemtask.moc"
diff --git a/resources/imap/autotests/testexpungecollectiontask.cpp b/resources/imap/autotests/testexpungecollectiontask.cpp
new file mode 100644 (file)
index 0000000..2dfc0a9
--- /dev/null
@@ -0,0 +1,127 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "imaptestbase.h"
+
+#include "expungecollectiontask.h"
+
+class TestExpungeCollectionTask : public ImapTestBase
+{
+    Q_OBJECT
+
+private Q_SLOTS:
+    void shouldDeleteMailBox_data()
+    {
+        QTest::addColumn<Akonadi::Collection>("collection");
+        QTest::addColumn< QList<QByteArray> >("scenario");
+        QTest::addColumn<QStringList>("callNames");
+
+        Akonadi::Collection collection;
+        QSet<QByteArray> parts;
+        QString messageContent;
+        QList<QByteArray> scenario;
+        QStringList callNames;
+
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 OK select done"
+                 << "C: A000004 EXPUNGE"
+                 << "S: A000004 OK expunge done";
+
+        callNames.clear();
+        callNames << QStringLiteral("taskDone");
+
+        QTest::newRow("normal case") << collection << scenario << callNames;
+
+        // We keep the same collection
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 NO select failed";
+
+        callNames.clear();
+        callNames << QStringLiteral("cancelTask");
+
+        QTest::newRow("select failed") << collection << scenario << callNames;
+
+        // We keep the same collection
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 OK select done"
+                 << "C: A000004 EXPUNGE"
+                 << "S: A000004 NO expunge failed";
+
+        callNames.clear();
+        callNames << QStringLiteral("cancelTask");
+
+        QTest::newRow("expunge failed") << collection << scenario << callNames;
+    }
+
+    void shouldDeleteMailBox()
+    {
+        QFETCH(Akonadi::Collection, collection);
+        QFETCH(QList<QByteArray>, scenario);
+        QFETCH(QStringList, callNames);
+
+        FakeServer server;
+        server.setScenario(scenario);
+        server.startAndWait();
+
+        SessionPool pool(1);
+
+        pool.setPasswordRequester(createDefaultRequester());
+        QVERIFY(pool.connect(createDefaultAccount()));
+        QVERIFY(waitForSignal(&pool, SIGNAL(connectDone(int,QString))));
+
+        DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState);
+        state->setCollection(collection);
+        ExpungeCollectionTask *task = new ExpungeCollectionTask(state);
+        task->start(&pool);
+
+        QTRY_COMPARE(state->calls().count(), callNames.size());
+        for (int i = 0; i < callNames.size(); i++) {
+            QString command = QString::fromUtf8(state->calls().at(i).first);
+            QVariant parameter = state->calls().at(i).second;
+
+            if (command == QLatin1String("cancelTask") && callNames[i] != QLatin1String("cancelTask")) {
+                qDebug() << "Got a cancel:" << parameter.toString();
+            }
+
+            QCOMPARE(command, callNames[i]);
+
+            if (command == QLatin1String("cancelTask")) {
+                QVERIFY(!parameter.toString().isEmpty());
+            }
+        }
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+};
+
+QTEST_GUILESS_MAIN(TestExpungeCollectionTask)
+
+#include "testexpungecollectiontask.moc"
diff --git a/resources/imap/autotests/testmovecollectiontask.cpp b/resources/imap/autotests/testmovecollectiontask.cpp
new file mode 100644 (file)
index 0000000..4842c5b
--- /dev/null
@@ -0,0 +1,195 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "imaptestbase.h"
+
+#include "movecollectiontask.h"
+
+class TestMoveCollectionTask : public ImapTestBase
+{
+    Q_OBJECT
+
+private Q_SLOTS:
+    void shouldRenameMailBox_data()
+    {
+        QTest::addColumn<Akonadi::Collection>("collection");
+        QTest::addColumn<Akonadi::Collection>("source");
+        QTest::addColumn<Akonadi::Collection>("target");
+        QTest::addColumn< QList<QByteArray> >("scenario");
+        QTest::addColumn<QStringList>("callNames");
+
+        Akonadi::Collection root;
+        Akonadi::Collection inbox;
+        Akonadi::Collection collection;
+        Akonadi::Collection source;
+        Akonadi::Collection target;
+        QList<QByteArray> scenario;
+        QStringList callNames;
+
+        root = createCollectionChain(QString());
+        inbox = createCollectionChain(QStringLiteral("/INBOX"));
+
+        source = Akonadi::Collection(3);
+        source.setRemoteId(QStringLiteral("/Foo"));
+        source.setParentCollection(inbox);
+
+        collection = Akonadi::Collection(10);
+        collection.setRemoteId(QStringLiteral("/Baz"));
+        collection.setParentCollection(source);
+
+        target = Akonadi::Collection(4);
+        target.setRemoteId(QStringLiteral("/Bar"));
+        target.setParentCollection(inbox);
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 RENAME \"INBOX/Foo/Baz\" \"INBOX/Bar/Baz\""
+                 << "S: A000003 OK rename done"
+                 << "C: A000004 SUBSCRIBE \"INBOX/Bar/Baz\""
+                 << "S: A000004 OK subscribe done";
+
+        callNames.clear();
+        callNames << QStringLiteral("collectionChangeCommitted");
+
+        QTest::newRow("moving mailbox") << collection << source << target << scenario << callNames;
+
+        {
+            const Akonadi::Collection toplevel = createCollectionChain(QStringLiteral("/Bar"));
+
+            scenario.clear();
+            scenario << defaultPoolConnectionScenario()
+                     << "C: A000003 RENAME \"Bar\" \"INBOX/Bar\""
+                     << "S: A000003 OK rename done"
+                     << "C: A000004 SUBSCRIBE \"INBOX/Bar\""
+                     << "S: A000004 OK subscribe done";
+
+            callNames.clear();
+            callNames << QStringLiteral("collectionChangeCommitted");
+
+            QTest::newRow("move mailbox from toplevel") << toplevel << root << inbox << scenario << callNames;
+        }
+
+        {
+            const Akonadi::Collection toplevel = createCollectionChain(QStringLiteral("/INBOX/Bar"));
+
+            scenario.clear();
+            scenario << defaultPoolConnectionScenario()
+                     << "C: A000003 RENAME \"INBOX/Bar\" \"Bar\""
+                     << "S: A000003 OK rename done"
+                     << "C: A000004 SUBSCRIBE \"Bar\""
+                     << "S: A000004 OK subscribe done";
+
+            callNames.clear();
+            callNames << QStringLiteral("collectionChangeCommitted");
+
+            QTest::newRow("move mailbox to toplevel") << toplevel << inbox << root << scenario << callNames;
+        }
+
+        // Same collections
+        // The scenario changes though
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 RENAME \"INBOX/Foo/Baz\" \"INBOX/Bar/Baz\""
+                 << "S: A000003 OK rename done"
+                 << "C: A000004 SUBSCRIBE \"INBOX/Bar/Baz\""
+                 << "S: A000004 NO subscribe failed";
+
+        callNames.clear();
+        callNames << QStringLiteral("emitWarning") << QStringLiteral("collectionChangeCommitted");
+
+        QTest::newRow("moving mailbox, subscribe fails") << collection << source << target << scenario << callNames;
+
+        inbox = createCollectionChain(QStringLiteral(".INBOX"));
+
+        source = Akonadi::Collection(3);
+        source.setRemoteId(QStringLiteral(".Foo"));
+        source.setParentCollection(inbox);
+
+        collection = Akonadi::Collection(10);
+        collection.setRemoteId(QStringLiteral(".Baz"));
+        collection.setParentCollection(source);
+
+        target = Akonadi::Collection(4);
+        target.setRemoteId(QStringLiteral(".Bar"));
+        target.setParentCollection(inbox);
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 RENAME \"INBOX.Foo.Baz\" \"INBOX.Bar.Baz\""
+                 << "S: A000003 OK rename done"
+                 << "C: A000004 SUBSCRIBE \"INBOX.Bar.Baz\""
+                 << "S: A000004 OK subscribe done";
+
+        callNames.clear();
+        callNames << QStringLiteral("collectionChangeCommitted");
+
+        QTest::newRow("moving mailbox with non-standard separators") << collection << source << target << scenario << callNames;
+    }
+
+    void shouldRenameMailBox()
+    {
+        QFETCH(Akonadi::Collection, collection);
+        QFETCH(Akonadi::Collection, source);
+        QFETCH(Akonadi::Collection, target);
+        QFETCH(QList<QByteArray>, scenario);
+        QFETCH(QStringList, callNames);
+
+        FakeServer server;
+        server.setScenario(scenario);
+        server.startAndWait();
+
+        SessionPool pool(1);
+
+        pool.setPasswordRequester(createDefaultRequester());
+        QVERIFY(pool.connect(createDefaultAccount()));
+        QVERIFY(waitForSignal(&pool, SIGNAL(connectDone(int,QString))));
+
+        DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState);
+        state->setCollection(collection);
+        state->setSourceCollection(source);
+        state->setTargetCollection(target);
+        MoveCollectionTask *task = new MoveCollectionTask(state);
+        task->start(&pool);
+
+        QTRY_COMPARE(state->calls().count(), callNames.size());
+        for (int i = 0; i < callNames.size(); i++) {
+            QString command = QString::fromUtf8(state->calls().at(i).first);
+            QVariant parameter = state->calls().at(i).second;
+
+            if (command == QLatin1String("cancelTask") && callNames[i] != QLatin1String("cancelTask")) {
+                qDebug() << "Got a cancel:" << parameter.toString();
+            }
+
+            QCOMPARE(command, callNames[i]);
+
+            if (command == QLatin1String("cancelTask")) {
+                QVERIFY(!parameter.toString().isEmpty());
+            }
+        }
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+};
+
+QTEST_GUILESS_MAIN(TestMoveCollectionTask)
+
+#include "testmovecollectiontask.moc"
diff --git a/resources/imap/autotests/testmoveitemstask.cpp b/resources/imap/autotests/testmoveitemstask.cpp
new file mode 100644 (file)
index 0000000..8fedaa7
--- /dev/null
@@ -0,0 +1,254 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "imaptestbase.h"
+
+#include "moveitemstask.h"
+#include "uidnextattribute.h"
+
+#include <kmime/kmime_message.h>
+
+Q_DECLARE_METATYPE(QSet<QByteArray>)
+
+class TestMoveItemsTask : public ImapTestBase
+{
+    Q_OBJECT
+
+private Q_SLOTS:
+    void shouldCopyAndDeleteMessage_data()
+    {
+        QTest::addColumn<Akonadi::Item>("item");
+        QTest::addColumn<Akonadi::Collection>("source");
+        QTest::addColumn<Akonadi::Collection>("target");
+        QTest::addColumn< QList<QByteArray> >("scenario");
+        QTest::addColumn<QStringList>("callNames");
+
+        Akonadi::Item item;
+        Akonadi::Collection inbox;
+        Akonadi::Collection source;
+        Akonadi::Collection target;
+        QList<QByteArray> scenario;
+        QStringList callNames;
+
+        item = Akonadi::Item(1);
+        item.setRemoteId(QStringLiteral("5"));
+
+        KMime::Message::Ptr message(new KMime::Message);
+
+        QString messageContent = QStringLiteral("From: ervin\nTo: someone\nSubject: foo\n\nSpeechless...");
+
+        message->setContent(messageContent.toUtf8());
+        message->parse();
+        item.setPayload(message);
+
+        inbox = createCollectionChain(QStringLiteral("/INBOX"));
+        source = Akonadi::Collection(3);
+        source.setRemoteId(QStringLiteral("/Foo"));
+        source.setParentCollection(inbox);
+        target = Akonadi::Collection(4);
+        target.setRemoteId(QStringLiteral("/Bar"));
+        target.setParentCollection(inbox);
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 OK select done"
+                 << "C: A000004 UID COPY 5 \"INBOX/Bar\""
+                 << "S: A000004 OK copy [ COPYUID 1239890035 5 65 ]"
+                 << "C: A000005 UID STORE 5 +FLAGS (\\Deleted)"
+                 << "S: A000005 OK store done";
+
+        callNames.clear();
+        callNames << QStringLiteral("itemsChangesCommitted");
+
+        QTest::newRow("moving mail") << item << source << target << scenario << callNames;
+
+        // Same item and collections
+        // The scenario changes though
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 OK select done"
+                 << "C: A000004 UID COPY 5 \"INBOX/Bar\""
+                 << "S: A000004 OK copy [ COPYUID 1239890035 5 65 ]"
+                 << "C: A000005 UID STORE 5 +FLAGS (\\Deleted)"
+                 << "S: A000005 NO store failed";
+
+        callNames.clear();
+        callNames << QStringLiteral("emitWarning") << QStringLiteral("itemsChangesCommitted");
+
+        QTest::newRow("moving mail, store fails") << item << source << target << scenario << callNames;
+
+        item = Akonadi::Item(1);
+        item.setRemoteId(QStringLiteral("5"));
+
+        message = KMime::Message::Ptr(new KMime::Message);
+
+        messageContent = QStringLiteral("From: ervin\nTo: someone\nSubject: foo\nMessage-ID: <42.4242.foo@bar.org>\n\nSpeechless...");
+
+        message->setContent(messageContent.toUtf8());
+        message->parse();
+        item.setPayload(message);
+
+        source = Akonadi::Collection(3);
+        source.setRemoteId(QStringLiteral("/Foo"));
+        source.setParentCollection(inbox);
+        source.addAttribute(new UidNextAttribute(42));
+        target = Akonadi::Collection(3);
+        target.setRemoteId(QStringLiteral("/Bar"));
+        target.setParentCollection(inbox);
+        target.addAttribute(new UidNextAttribute(65));
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 OK select done"
+                 << "C: A000004 UID COPY 5 \"INBOX/Bar\""
+                 << "S: A000004 OK copy done"
+                 << "C: A000005 UID STORE 5 +FLAGS (\\Deleted)"
+                 << "S: A000005 OK store done"
+                 << "C: A000006 SELECT \"INBOX/Bar\""
+                 << "S: A000006 OK select done"
+                 << "C: A000007 UID SEARCH (HEADER Message-ID <42.4242.foo@bar.org>)"
+                 << "S: * SEARCH 65"
+                 << "S: A000007 OK search done";
+
+        callNames.clear();
+        callNames << QStringLiteral("itemsChangesCommitted") << QStringLiteral("applyCollectionChanges");
+
+        QTest::newRow("moving mail, no COPYUID, message had Message-ID") << item << source << target << scenario << callNames;
+
+        item = Akonadi::Item(1);
+        item.setRemoteId(QStringLiteral("5"));
+
+        message = KMime::Message::Ptr(new KMime::Message);
+
+        messageContent = QStringLiteral("From: ervin\nTo: someone\nSubject: foo\n\nSpeechless...");
+
+        message->setContent(messageContent.toUtf8());
+        message->parse();
+        item.setPayload(message);
+
+        source = Akonadi::Collection(3);
+        source.setRemoteId(QStringLiteral("/Foo"));
+        source.setParentCollection(inbox);
+        source.addAttribute(new UidNextAttribute(42));
+        target = Akonadi::Collection(4);
+        target.setRemoteId(QStringLiteral("/Bar"));
+        target.setParentCollection(inbox);
+        target.addAttribute(new UidNextAttribute(65));
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 OK select done"
+                 << "C: A000004 UID COPY 5 \"INBOX/Bar\""
+                 << "S: A000004 OK copy done"
+                 << "C: A000005 UID STORE 5 +FLAGS (\\Deleted)"
+                 << "S: A000005 OK store done"
+                 << "C: A000006 SELECT \"INBOX/Bar\""
+                 << "S: A000006 OK select done"
+                 << "C: A000007 UID SEARCH NEW UID 65:*"
+                 << "S: * SEARCH 65"
+                 << "S: A000007 OK search done";
+
+        callNames.clear();
+        callNames << QStringLiteral("itemsChangesCommitted") << QStringLiteral("applyCollectionChanges");
+
+        QTest::newRow("moving mail, no COPYUID, message didn't have Message-ID") << item << source << target << scenario << callNames;
+
+        item = Akonadi::Item(1);
+        item.setRemoteId(QStringLiteral("5"));
+        message = KMime::Message::Ptr(new KMime::Message);
+        messageContent = QStringLiteral("From: ervin\nTo: someone\nSubject: foo\nMessage-ID: <42.4242.foo@bar.org>\n\nSpeechless...");
+        message->setContent(messageContent.toUtf8());
+        message->parse();
+        item.setPayload(message);
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 OK select done"
+                 << "C: A000004 UID COPY 5 \"INBOX/Bar\""
+                 << "S: A000004 OK copy done"
+                 << "C: A000005 UID STORE 5 +FLAGS (\\Deleted)"
+                 << "S: A000005 OK store done"
+                 << "C: A000006 SELECT \"INBOX/Bar\""
+                 << "S: A000006 OK select done"
+                 << "C: A000007 UID SEARCH (HEADER Message-ID <42.4242.foo@bar.org>)"
+                 << "S: * SEARCH 61 65"
+                 << "S: A000007 OK search done";
+
+        callNames.clear();
+        callNames << QStringLiteral("itemsChangesCommitted") << QStringLiteral("applyCollectionChanges");
+
+        QTest::newRow("moving mail, no COPYUID, message didn't have unique Message-ID, but last one matches old uidnext") << item << source << target << scenario << callNames;
+    }
+
+    void shouldCopyAndDeleteMessage()
+    {
+        QFETCH(Akonadi::Item, item);
+        QFETCH(Akonadi::Collection, source);
+        QFETCH(Akonadi::Collection, target);
+        QFETCH(QList<QByteArray>, scenario);
+        QFETCH(QStringList, callNames);
+
+        FakeServer server;
+        server.setScenario(scenario);
+        server.startAndWait();
+
+        SessionPool pool(1);
+
+        pool.setPasswordRequester(createDefaultRequester());
+        QVERIFY(pool.connect(createDefaultAccount()));
+        QVERIFY(waitForSignal(&pool, SIGNAL(connectDone(int,QString))));
+
+        DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState);
+        state->setItem(item);
+        state->setSourceCollection(source);
+        state->setTargetCollection(target);
+        MoveItemsTask *task = new MoveItemsTask(state);
+        task->start(&pool);
+
+        QTRY_COMPARE(state->calls().count(), callNames.size());
+        for (int i = 0; i < callNames.size(); i++) {
+            QString command = QString::fromUtf8(state->calls().at(i).first);
+            QVariant parameter = state->calls().at(i).second;
+
+            if (command == QLatin1String("cancelTask") && callNames[i] != QLatin1String("cancelTask")) {
+                qDebug() << "Got a cancel:" << parameter.toString();
+            }
+
+            QCOMPARE(command, callNames[i]);
+
+            if (command == QLatin1String("cancelTask")) {
+                QVERIFY(!parameter.toString().isEmpty());
+            }
+        }
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+};
+
+QTEST_GUILESS_MAIN(TestMoveItemsTask)
+
+#include "testmoveitemstask.moc"
diff --git a/resources/imap/autotests/testremovecollectionrecursivetask.cpp b/resources/imap/autotests/testremovecollectionrecursivetask.cpp
new file mode 100644 (file)
index 0000000..aa37abe
--- /dev/null
@@ -0,0 +1,247 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "imaptestbase.h"
+
+#include "removecollectionrecursivetask.h"
+
+class TestRemoveCollectionRecursiveTask : public ImapTestBase
+{
+    Q_OBJECT
+
+    void shouldDeleteMailBoxRecursive_data()
+    {
+        QTest::addColumn<Akonadi::Collection>("collection");
+        QTest::addColumn< QList<QByteArray> >("scenario");
+        QTest::addColumn<QStringList>("callNames");
+
+        Akonadi::Collection collection;
+        QList<QByteArray> scenario;
+        QStringList callNames;
+
+        collection = createCollectionChain(QStringLiteral("/INBOX/test1"));
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 LSUB \"\" *"
+                 << "S: * LSUB ( \\HasChildren ) / INBOX"
+                 << "S: * LSUB ( \\HasChildren ) / INBOX/test1"
+                 << "S: * LSUB ( ) / INBOX/test1/test2"
+                 << "S: A000003 OK Completed ( 0.000 secs 26 calls )"
+                 << "C: A000004 SELECT \"INBOX/test1/test2\""
+                 << "S: * FLAGS ( \\Answered \\Flagged \\Draft \\Deleted \\Seen )"
+                 << "S: * OK [ PERMANENTFLAGS ( \\Answered \\Flagged \\Draft \\Deleted \\Seen \\* )  ]"
+                 << "S: * 1 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: * OK [ UNSEEN 1  ]"
+                 << "S: * OK [ UIDVALIDITY 1292857898  ]"
+                 << "S: * OK [ UIDNEXT 2  ]"
+                 << "S: A000004 OK Completed [ READ-WRITE  ]"
+                 << "C: A000005 STORE 1:* +FLAGS (\\DELETED)"
+                 << "S: * 1 FETCH ( FLAGS (\\Deleted) ) "
+                 << "S: A000005 OK Completed"
+                 << "C: A000006 EXPUNGE"
+                 << "S: * 1 EXPUNGE"
+                 << "S: * 0 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: A000006 OK Completed"
+                 << "C: A000007 CLOSE"
+                 << "S: A000007 OK Completed"
+                 << "C: A000008 DELETE \"INBOX/test1/test2\""
+                 << "S: * 0 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: A000008 OK Completed"
+                 << "C: A000009 SELECT \"INBOX/test1\""
+                 << "S: * FLAGS ( \\Answered \\Flagged \\Draft \\Deleted \\Seen )"
+                 << "S: * OK [ PERMANENTFLAGS ( \\Answered \\Flagged \\Draft \\Deleted \\Seen \\* )  ]"
+                 << "S: * 1 EXISTS"
+                 << "S: * 1 RECENT"
+                 << "S: * OK [ UIDVALIDITY 1292857888  ]"
+                 << "S: * OK [ UIDNEXT 2  ]"
+                 << "S: A000009 OK Completed [ READ-WRITE  ]"
+                 << "C: A000010 STORE 1:* +FLAGS (\\DELETED)"
+                 << "S: * 1 FETCH ( FLAGS (\\Recent \\Deleted \\Seen) )"
+                 << "S: A000010 OK Completed"
+                 << "C: A000011 EXPUNGE"
+                 << "S: * 1 EXPUNGE"
+                 << "S: * 0 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: A000011 OK Completed"
+                 << "C: A000012 CLOSE"
+                 << "S: A000012 OK Completed"
+                 << "C: A000013 DELETE \"INBOX/test1\""
+                 << "S: * 0 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: A000013 OK Completed";
+        callNames.clear();
+        callNames << QStringLiteral("changeProcessed");
+
+        QTest::newRow("normal case") << collection << scenario << callNames;
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 LSUB \"\" *"
+                 << "S: * LSUB ( \\HasChildren ) / INBOX"
+                 << "S: * LSUB ( \\HasChildren ) / INBOX/test1"
+                 << "S: * LSUB ( ) / INBOX/test1/test2"
+                 << "S: A000003 OK Completed ( 0.000 secs 26 calls )";
+        collection.setRemoteId(QStringLiteral("/test1"));
+        collection.setParentCollection(Akonadi::Collection::root());
+        callNames.clear();
+        callNames << QStringLiteral("changeProcessed") << QStringLiteral("emitWarning") << QStringLiteral("synchronizeCollectionTree");
+        QTest::newRow("invalid collection") << collection << scenario << callNames;
+
+        collection = createCollectionChain(QStringLiteral(".INBOX.test1"));
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 LSUB \"\" *"
+                 << "S: * LSUB ( \\HasChildren ) . INBOX"
+                 << "S: * LSUB ( \\HasChildren ) . INBOX.test1"
+                 << "S: * LSUB ( ) . INBOX.test1.test2"
+                 << "S: A000003 OK Completed ( 0.000 secs 26 calls )"
+                 << "C: A000004 SELECT \"INBOX.test1.test2\""
+                 << "S: * FLAGS ( \\Answered \\Flagged \\Draft \\Deleted \\Seen )"
+                 << "S: * OK [ PERMANENTFLAGS ( \\Answered \\Flagged \\Draft \\Deleted \\Seen \\* )  ]"
+                 << "S: * 1 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: * OK [ UNSEEN 1  ]"
+                 << "S: * OK [ UIDVALIDITY 1292857898  ]"
+                 << "S: * OK [ UIDNEXT 2  ]"
+                 << "S: A000004 OK Completed [ READ-WRITE  ]"
+                 << "C: A000005 STORE 1:* +FLAGS (\\DELETED)"
+                 << "S: * 1 FETCH ( FLAGS (\\Deleted) ) "
+                 << "S: A000005 OK Completed"
+                 << "C: A000006 EXPUNGE"
+                 << "S: * 1 EXPUNGE"
+                 << "S: * 0 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: A000006 OK Completed"
+                 << "C: A000007 CLOSE"
+                 << "S: A000007 OK Completed"
+                 << "C: A000008 DELETE \"INBOX.test1.test2\""
+                 << "S: * 0 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: A000008 OK Completed"
+                 << "C: A000009 SELECT \"INBOX.test1\""
+                 << "S: * FLAGS ( \\Answered \\Flagged \\Draft \\Deleted \\Seen )"
+                 << "S: * OK [ PERMANENTFLAGS ( \\Answered \\Flagged \\Draft \\Deleted \\Seen \\* )  ]"
+                 << "S: * 1 EXISTS"
+                 << "S: * 1 RECENT"
+                 << "S: * OK [ UIDVALIDITY 1292857888  ]"
+                 << "S: * OK [ UIDNEXT 2  ]"
+                 << "S: A000009 OK Completed [ READ-WRITE  ]"
+                 << "C: A000010 STORE 1:* +FLAGS (\\DELETED)"
+                 << "S: * 1 FETCH ( FLAGS (\\Recent \\Deleted \\Seen) )"
+                 << "S: A000010 OK Completed"
+                 << "C: A000011 EXPUNGE"
+                 << "S: * 1 EXPUNGE"
+                 << "S: * 0 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: A000011 OK Completed"
+                 << "C: A000012 CLOSE"
+                 << "S: A000012 OK Completed"
+                 << "C: A000013 DELETE \"INBOX.test1\""
+                 << "S: * 0 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: A000013 OK Completed";
+        callNames.clear();
+        callNames << QStringLiteral("changeProcessed");
+        QTest::newRow("non-standard separator") << collection << scenario << callNames;
+
+        collection = createCollectionChain(QStringLiteral(".INBOX.test1"));
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 LSUB \"\" *"
+                 << "S: * LSUB ( \\HasChildren ) . INBOX"
+                 << "S: * LSUB ( \\HasChildren ) . INBOX.test1"
+                 << "S: * LSUB ( ) . INBOX.test1.test2"
+                 << "S: A000003 OK Completed ( 0.000 secs 26 calls )"
+                 << "C: A000004 SELECT \"INBOX.test1.test2\""
+                 << "S: * FLAGS ( \\Answered \\Flagged \\Draft \\Deleted \\Seen )"
+                 << "S: * OK [ PERMANENTFLAGS ( \\Answered \\Flagged \\Draft \\Deleted \\Seen \\* )  ]"
+                 << "S: * 1 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: * OK [ UNSEEN 1  ]"
+                 << "S: * OK [ UIDVALIDITY 1292857898  ]"
+                 << "S: * OK [ UIDNEXT 2  ]"
+                 << "S: A000004 OK Completed [ READ-WRITE  ]"
+                 << "C: A000005 STORE 1:* +FLAGS (\\DELETED)"
+                 << "S: * 1 FETCH ( FLAGS (\\Deleted) ) "
+                 << "S: A000005 OK Completed"
+                 << "C: A000006 EXPUNGE"
+                 << "S: * 1 EXPUNGE"
+                 << "S: * 0 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: A000006 OK Completed"
+                 << "C: A000007 CLOSE"
+                 << "S: A000007 NO Close failed";
+        callNames.clear();
+        callNames << QStringLiteral("changeProcessed") << QStringLiteral("emitWarning") << QStringLiteral("synchronizeCollectionTree");
+        QTest::newRow("close failed") << collection << scenario << callNames;
+    }
+
+    void shouldDeleteMailBoxRecursive()
+    {
+        QFETCH(Akonadi::Collection, collection);
+        QFETCH(QList<QByteArray>, scenario);
+        QFETCH(QStringList, callNames);
+
+        FakeServer server;
+        server.setScenario(scenario);
+        server.startAndWait();
+
+        SessionPool pool(1);
+
+        pool.setPasswordRequester(createDefaultRequester());
+        QVERIFY(pool.connect(createDefaultAccount()));
+        QVERIFY(waitForSignal(&pool, SIGNAL(connectDone(int,QString))));
+
+        DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState);
+        state->setCollection(collection);
+        RemoveCollectionRecursiveTask *task = new RemoveCollectionRecursiveTask(state);
+        task->start(&pool);
+        QEventLoop loop;
+        connect(task, &RemoveCollectionRecursiveTask::destroyed, &loop, &QEventLoop::quit);
+        loop.exec();
+
+        QCOMPARE(state->calls().count(), callNames.size());
+        for (int i = 0; i < callNames.size(); i++) {
+            QString command = QString::fromUtf8(state->calls().at(i).first);
+            QVariant parameter = state->calls().at(i).second;
+
+            if (command == QLatin1String("cancelTask") && callNames[i] != QLatin1String("cancelTask")) {
+                qDebug() << "Got a cancel:" << parameter.toString();
+            }
+
+            QCOMPARE(command, callNames[i]);
+
+            if (command == QLatin1String("cancelTask")) {
+                QVERIFY(!parameter.toString().isEmpty());
+            }
+        }
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+};
+
+QTEST_GUILESS_MAIN(TestRemoveCollectionRecursiveTask)
+
+#include "testremovecollectionrecursivetask.moc"
diff --git a/resources/imap/autotests/testresourcetask.cpp b/resources/imap/autotests/testresourcetask.cpp
new file mode 100644 (file)
index 0000000..80d6714
--- /dev/null
@@ -0,0 +1,173 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "imaptestbase.h"
+#include <QSignalSpy>
+#include "resourcetask.h"
+#include <KLocalizedString>
+
+Q_DECLARE_METATYPE(ResourceTask::ActionIfNoSession)
+
+class DummyResourceTask : public ResourceTask
+{
+public:
+    explicit DummyResourceTask(ActionIfNoSession action, ResourceStateInterface::Ptr resource, QObject *parent = Q_NULLPTR)
+        : ResourceTask(action, resource, parent)
+    {
+
+    }
+
+    void doStart(KIMAP::Session */*session*/)
+    {
+        cancelTask(QStringLiteral("Dummy task"));
+    }
+};
+
+class TestResourceTask : public ImapTestBase
+{
+    Q_OBJECT
+
+private Q_SLOTS:
+    void shouldRequestSession_data()
+    {
+        QTest::addColumn<DummyResourceState::Ptr>("state");
+        QTest::addColumn< QList<QByteArray> >("scenario");
+        QTest::addColumn<bool>("shouldConnect");
+        QTest::addColumn<bool>("shouldRequestSession");
+        QTest::addColumn<ResourceTask::ActionIfNoSession>("actionIfNoSession");
+        QTest::addColumn<QStringList>("callNames");
+        QTest::addColumn<QVariant>("firstCallParameter");
+
+        DummyResourceState::Ptr state;
+        QList<QByteArray> scenario;
+        QStringList callNames;
+
+        state = DummyResourceState::Ptr(new DummyResourceState);
+        scenario.clear();
+        scenario << FakeServer::greeting()
+                 << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""
+                 << "S: A000001 OK User Logged in"
+                 << "C: A000002 CAPABILITY"
+                 << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE"
+                 << "S: A000002 OK Completed";
+        callNames.clear();
+        callNames << QStringLiteral("cancelTask");
+        QTest::newRow("normal case") << state << scenario
+                                     << true << false
+                                     << ResourceTask::DeferIfNoSession
+                                     << callNames << QVariant("Dummy task");
+
+        state = DummyResourceState::Ptr(new DummyResourceState);
+        callNames.clear();
+        callNames << QStringLiteral("deferTask");
+        QTest::newRow("all sessions allocated (defer)") << state << scenario
+                << true << true
+                << ResourceTask::DeferIfNoSession
+                << callNames << QVariant();
+
+        state = DummyResourceState::Ptr(new DummyResourceState);
+        callNames.clear();
+        callNames << QStringLiteral("cancelTask");
+        QTest::newRow("all sessions allocated (cancel)") << state << scenario
+                << true << true
+                << ResourceTask::CancelIfNoSession
+                << callNames << QVariant();
+
+        state = DummyResourceState::Ptr(new DummyResourceState);
+        scenario.clear();
+        callNames.clear();
+        callNames << QStringLiteral("deferTask") << QStringLiteral("scheduleConnectionAttempt");
+        QTest::newRow("disconnected pool (defer)") << state << scenario
+                << false << false
+                << ResourceTask::DeferIfNoSession
+                << callNames << QVariant();
+
+        state = DummyResourceState::Ptr(new DummyResourceState);
+        scenario.clear();
+        callNames.clear();
+        callNames << QStringLiteral("cancelTask") << QStringLiteral("scheduleConnectionAttempt");
+        QTest::newRow("disconnected pool (cancel)") << state << scenario
+                << false << false
+                << ResourceTask::CancelIfNoSession
+                << callNames << QVariant();
+    }
+
+    void shouldRequestSession()
+    {
+        QFETCH(DummyResourceState::Ptr, state);
+        QFETCH(QList<QByteArray>, scenario);
+        QFETCH(bool, shouldConnect);
+        QFETCH(bool, shouldRequestSession);
+        QFETCH(ResourceTask::ActionIfNoSession, actionIfNoSession);
+        QFETCH(QStringList, callNames);
+        QFETCH(QVariant, firstCallParameter);
+
+        FakeServer server;
+        server.setScenario(scenario);
+        server.startAndWait();
+
+        SessionPool pool(1);
+
+        if (shouldConnect) {
+            QSignalSpy poolSpy(&pool, SIGNAL(connectDone(int,QString)));
+
+            pool.setPasswordRequester(createDefaultRequester());
+            QVERIFY(pool.connect(createDefaultAccount()));
+
+            QTRY_COMPARE(poolSpy.count(), 1);
+            QCOMPARE(poolSpy.at(0).at(0).toInt(), (int)SessionPool::NoError);
+        }
+
+        if (shouldRequestSession) {
+            QSignalSpy requestSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session*,int,QString)));
+            pool.requestSession();
+            QTRY_COMPARE(requestSpy.count(), 1);
+        }
+
+        QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session*,int,QString)));
+        DummyResourceTask *task = new DummyResourceTask(actionIfNoSession, state);
+        task->start(&pool);
+
+        if (shouldConnect) {
+            QTRY_COMPARE(sessionSpy.count(), 1);
+        } else {
+            //We want to ensure the signal isn't emitted, so we have to wait
+            QTest::qWait(500);
+            QCOMPARE(sessionSpy.count(), 0);
+        }
+
+        QCOMPARE(state->calls().count(), callNames.size());
+        for (int i = 0; i < callNames.size(); i++) {
+            QString command = QString::fromUtf8(state->calls().at(i).first);
+            QCOMPARE(command, callNames[i]);
+        }
+
+        if (firstCallParameter.toString() == QLatin1String("Dummy task")) {
+            QCOMPARE(state->calls().first().second, firstCallParameter);
+        }
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+};
+
+QTEST_GUILESS_MAIN(TestResourceTask)
+
+#include "testresourcetask.moc"
diff --git a/resources/imap/autotests/testretrievecollectionmetadatatask.cpp b/resources/imap/autotests/testretrievecollectionmetadatatask.cpp
new file mode 100644 (file)
index 0000000..f352163
--- /dev/null
@@ -0,0 +1,324 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "imaptestbase.h"
+
+#include "retrievecollectionmetadatatask.h"
+
+#include <collectionquotaattribute.h>
+#include <attributefactory.h>
+#include "collectionannotationsattribute.h"
+#include "imapaclattribute.h"
+#include "imapquotaattribute.h"
+#include "noselectattribute.h"
+#include <noinferiorsattribute.h>
+
+typedef QMap<QByteArray, QByteArray> QBYTEARRAYMAP;
+
+Q_DECLARE_METATYPE(Akonadi::Collection::Rights)
+Q_DECLARE_METATYPE(QBYTEARRAYMAP)
+
+class TestRetrieveCollectionMetadataTask : public ImapTestBase
+{
+    Q_OBJECT
+
+private Q_SLOTS:
+
+    void initTestCase()
+    {
+        Akonadi::AttributeFactory::registerAttribute<Akonadi::ImapAclAttribute>();
+        Akonadi::AttributeFactory::registerAttribute<NoSelectAttribute>();
+    }
+
+    void shouldCollectionRetrieveMetadata_data()
+    {
+        QTest::addColumn<Akonadi::Collection>("collection");
+        QTest::addColumn<QStringList>("capabilities");
+        QTest::addColumn< QList<QByteArray> >("scenario");
+        QTest::addColumn<QStringList>("callNames");
+        QTest::addColumn<Akonadi::Collection::Rights>("expectedRights");
+        QTest::addColumn<QBYTEARRAYMAP>("expectedAnnotations");
+
+        Akonadi::Collection collection;
+        QStringList capabilities;
+        QList<QByteArray> scenario;
+        QStringList callNames;
+        QMap<QByteArray, QByteArray> expectedAnnotations;
+
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection.setRights(0);
+
+        capabilities.clear();
+        capabilities << QStringLiteral("ANNOTATEMORE") << QStringLiteral("ACL") << QStringLiteral("QUOTA");
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 GETANNOTATION \"INBOX/Foo\" \"*\" \"value.shared\""
+                 << "S: * ANNOTATION INBOX/Foo /vendor/kolab/folder-test ( value.shared true )"
+                 << "S: A000003 OK annotations retrieved"
+                 << "C: A000004 MYRIGHTS \"INBOX/Foo\""
+                 << "S: * MYRIGHTS \"INBOX/Foo\" lrswipkxtecda"
+                 << "S: A000004 OK rights retrieved"
+                 << "C: A000005 GETQUOTAROOT \"INBOX/Foo\""
+                 << "S: * QUOTAROOT INBOX/Foo user/foo"
+                 << "S: * QUOTA user/foo ( )"
+                 << "S: A000005 OK quota retrieved"
+                 << "C: A000006 GETACL \"INBOX/Foo\""
+                 << "S: * ACL INBOX/Foo foo@kde.org lrswipcda"
+                 << "S: A000006 OK acl retrieved";
+
+        callNames.clear();
+        callNames << QStringLiteral("collectionAttributesRetrieved");
+
+        expectedAnnotations.clear();
+        expectedAnnotations.insert("/shared/vendor/kolab/folder-test", "true");
+
+        Akonadi::Collection::Rights rights = Akonadi::Collection::AllRights;
+        QTest::newRow("first listing, connected IMAP") << collection << capabilities << scenario
+                << callNames << rights << expectedAnnotations;
+
+        //
+        // Test that if the parent collection doesn't allow renaming in its ACL, the child mailbox
+        // can't be renamed, i.e. doesn't have the CanChangeCollection flag.
+        //
+        Akonadi::Collection parentCollection = createCollectionChain(QStringLiteral("/INBOX"));
+        QMap<QByteArray, KIMAP::Acl::Rights> rightsMap;
+        rightsMap.insert("Hans", KIMAP::Acl::Lookup | KIMAP::Acl::Read | KIMAP::Acl::KeepSeen |
+                         KIMAP::Acl::Write | KIMAP::Acl::Insert | KIMAP::Acl::Post |
+                         KIMAP::Acl::Delete);
+        Akonadi::ImapAclAttribute *aclAttribute = new Akonadi::ImapAclAttribute();
+        aclAttribute->setRights(rightsMap);
+        parentCollection.addAttribute(aclAttribute);
+        collection.setParentCollection(parentCollection);
+        rights = Akonadi::Collection::AllRights;
+        rights &= ~Akonadi::Collection::CanChangeCollection;
+        QTest::newRow("parent without create rights") << collection << capabilities << scenario
+                << callNames << rights << expectedAnnotations;
+
+        //
+        // Test that if the parent collection is a noselect folder, the child mailbox will not have
+        // rename (CanChangeCollection) permission.
+        //
+        parentCollection = createCollectionChain(QStringLiteral("/INBOX"));
+        NoSelectAttribute *noSelectAttribute = new NoSelectAttribute();
+        parentCollection.addAttribute(noSelectAttribute);
+        collection.setParentCollection(parentCollection);
+        QTest::newRow("parent wit noselect") << collection << capabilities << scenario
+                                             << callNames << rights << expectedAnnotations;
+        parentCollection.removeAttribute<NoSelectAttribute>();
+
+        //
+        // Test that the rights are properly set on the resulting collection if the mailbox doesn't
+        // have full rights.
+        //
+        collection.setParentCollection(createCollectionChain(QStringLiteral("/INBOX")));
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 GETANNOTATION \"INBOX/Foo\" \"*\" \"value.shared\""
+                 << "S: * ANNOTATION INBOX/Foo /vendor/kolab/folder-test ( value.shared true )"
+                 << "S: A000003 OK annotations retrieved"
+                 << "C: A000004 MYRIGHTS \"INBOX/Foo\""
+                 << "S: * MYRIGHTS \"INBOX/Foo\" wi"
+                 << "S: A000004 OK rights retrieved"
+                 << "C: A000005 GETQUOTAROOT \"INBOX/Foo\""
+                 << "S: * QUOTAROOT INBOX/Foo user/foo"
+                 << "S: * QUOTA user/foo ( )"
+                 << "S: A000005 OK quota retrieved";
+        rights = Akonadi::Collection::CanCreateItem | Akonadi::Collection::CanChangeItem |
+                 Akonadi::Collection::CanChangeCollection;
+        QTest::newRow("only some rights") << collection << capabilities << scenario
+                                          << callNames << rights << expectedAnnotations;
+
+        //
+        // Test that a warning is issued if the insert rights of a folder have been revoked on the server.
+        //
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection.setParentCollection(createCollectionChain(QStringLiteral("/INBOX")));
+        //We use the aclattribute to determine if a collection already has acl's or not
+        collection.addAttribute(new Akonadi::ImapAclAttribute());
+        collection.setRights(Akonadi::Collection::CanCreateItem);
+
+        capabilities.clear();
+        capabilities << QStringLiteral("ANNOTATEMORE") << QStringLiteral("ACL") << QStringLiteral("QUOTA");
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 GETANNOTATION \"INBOX/Foo\" \"*\" \"value.shared\""
+                 << "S: * ANNOTATION INBOX/Foo /vendor/kolab/folder-test ( value.shared true )"
+                 << "S: A000003 OK annotations retrieved"
+                 << "C: A000004 MYRIGHTS \"INBOX/Foo\""
+                 << "S: * MYRIGHTS \"INBOX/Foo\" w"
+                 << "S: A000004 OK rights retrieved"
+                 << "C: A000005 GETQUOTAROOT \"INBOX/Foo\""
+                 << "S: * QUOTAROOT INBOX/Foo user/foo"
+                 << "S: * QUOTA user/foo ( )"
+                 << "S: A000005 OK quota retrieved";
+
+        callNames.clear();
+        callNames << QStringLiteral("showInformationDialog");
+        callNames << QStringLiteral("collectionAttributesRetrieved");
+
+        rights = Akonadi::Collection::CanChangeItem | Akonadi::Collection::CanChangeCollection;
+        QTest::newRow("revoked rights") << collection << capabilities << scenario
+                                        << callNames << rights << expectedAnnotations;
+
+        //
+        // Test that NoInferiors overrides acl rights and disallows creating new mailboxes
+        //
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection.setParentCollection(createCollectionChain(QString()));
+        collection.setRemoteId(QStringLiteral("/INBOX"));
+        collection.setRights(Akonadi::Collection::AllRights);
+        collection.addAttribute(new NoInferiorsAttribute(true));
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 GETANNOTATION \"INBOX\" \"*\" \"value.shared\""
+                 << "S: * ANNOTATION INBOX /vendor/kolab/folder-test ( value.shared true )"
+                 << "S: A000003 OK annotations retrieved"
+                 << "C: A000004 MYRIGHTS \"INBOX\""
+                 << "S: * MYRIGHTS \"INBOX\" wk"
+                 << "S: A000004 OK rights retrieved"
+                 << "C: A000005 GETQUOTAROOT \"INBOX\""
+                 << "S: * QUOTAROOT INBOX user"
+                 << "S: * QUOTA user ( )"
+                 << "S: A000005 OK quota retrieved";
+
+        callNames.clear();
+        callNames << QStringLiteral("collectionAttributesRetrieved");
+
+        rights = Akonadi::Collection::CanChangeItem | Akonadi::Collection::CanChangeCollection;
+
+        QTest::newRow("noinferiors") << collection << capabilities << scenario
+                                     << callNames << rights << expectedAnnotations;
+
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection.setRights(0);
+
+        capabilities.clear();
+        capabilities << QStringLiteral("METADATA") << QStringLiteral("ACL") << QStringLiteral("QUOTA");
+
+        expectedAnnotations.clear();
+        expectedAnnotations.insert("/shared/vendor/kolab/folder-test", "true");
+        expectedAnnotations.insert("/shared/vendor/kolab/folder-test2", "");
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 GETMETADATA (DEPTH infinity) \"INBOX/Foo\" (/shared)"
+                 << "S: * METADATA \"INBOX/Foo\" (/shared/vendor/kolab/folder-test \"true\")"
+                 << "S: * METADATA \"INBOX/Foo\" (/shared/vendor/kolab/folder-test2 \"NIL\")"
+                 << "S: * METADATA \"INBOX/Foo\" (/shared/vendor/cmu/cyrus-imapd/lastupdate \"true\")"
+                 << "S: A000003 OK GETMETADATA complete"
+                 << "C: A000004 MYRIGHTS \"INBOX/Foo\""
+                 << "S: * MYRIGHTS \"INBOX/Foo\" lrswipkxtecda"
+                 << "S: A000004 OK rights retrieved"
+                 << "C: A000005 GETQUOTAROOT \"INBOX/Foo\""
+                 << "S: * QUOTAROOT INBOX/Foo user/Foo"
+                 << "S: * QUOTA user/Foo ( )"
+                 << "S: A000005 OK quota retrieved"
+                 << "C: A000006 GETACL \"INBOX/Foo\""
+                 << "S: * ACL INBOX/Foo foo@kde.org lrswipcda"
+                 << "S: A000006 OK acl retrieved";
+
+        callNames.clear();
+        callNames << QStringLiteral("collectionAttributesRetrieved");
+
+        rights = Akonadi::Collection::AllRights;
+        QTest::newRow("METADATA") << collection << capabilities << scenario
+                                  << callNames << rights << expectedAnnotations;
+
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection.setRights(0);
+
+        capabilities.clear();
+        expectedAnnotations.clear();
+
+        callNames.clear();
+        callNames << QStringLiteral("collectionAttributesRetrieved");
+
+        rights = 0;
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario();
+
+        QTest::newRow("no capabilities") << collection << capabilities << scenario
+                                         << callNames << rights << expectedAnnotations;
+    }
+
+    void shouldCollectionRetrieveMetadata()
+    {
+        QFETCH(Akonadi::Collection, collection);
+        QFETCH(QStringList, capabilities);
+        QFETCH(QList<QByteArray>, scenario);
+        QFETCH(QStringList, callNames);
+        QFETCH(Akonadi::Collection::Rights, expectedRights);
+        QFETCH(QBYTEARRAYMAP, expectedAnnotations);
+
+        FakeServer server;
+        server.setScenario(scenario);
+        server.startAndWait();
+
+        SessionPool pool(1);
+
+        pool.setPasswordRequester(createDefaultRequester());
+        QVERIFY(pool.connect(createDefaultAccount()));
+        QVERIFY(waitForSignal(&pool, SIGNAL(connectDone(int,QString))));
+
+        DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState);
+        state->setCollection(collection);
+        state->setServerCapabilities(capabilities);
+        state->setUserName(QStringLiteral("Hans"));
+        RetrieveCollectionMetadataTask *task = new RetrieveCollectionMetadataTask(state);
+
+        task->start(&pool);
+
+        QTRY_COMPARE(state->calls().count(), callNames.size());
+        for (int i = 0; i < callNames.size(); i++) {
+            QString command = QString::fromUtf8(state->calls().at(i).first);
+            QVariant parameter = state->calls().at(i).second;
+
+            if (command == QLatin1String("cancelTask") && callNames[i] != QLatin1String("cancelTask")) {
+                qDebug() << "Got a cancel:" << parameter.toString();
+            }
+
+            QCOMPARE(command, callNames[i]);
+
+            if (command == QLatin1String("cancelTask")) {
+                QVERIFY(!parameter.toString().isEmpty());
+            }
+
+            if (command == QLatin1String("collectionAttributesRetrieved")) {
+                Akonadi::Collection collection = parameter.value<Akonadi::Collection>();
+                QCOMPARE(collection.rights(), expectedRights);
+
+                if (!expectedAnnotations.isEmpty()) {
+                    QVERIFY(collection.hasAttribute<Akonadi::CollectionAnnotationsAttribute>());
+                    QCOMPARE(collection.attribute<Akonadi::CollectionAnnotationsAttribute>()->annotations(), expectedAnnotations);
+                }
+            }
+        }
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+};
+
+QTEST_GUILESS_MAIN(TestRetrieveCollectionMetadataTask)
+
+#include "testretrievecollectionmetadatatask.moc"
diff --git a/resources/imap/autotests/testretrievecollectionstask.cpp b/resources/imap/autotests/testretrievecollectionstask.cpp
new file mode 100644 (file)
index 0000000..aef1b1a
--- /dev/null
@@ -0,0 +1,480 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "imaptestbase.h"
+
+#include "retrievecollectionstask.h"
+#include "noselectattribute.h"
+#include <noinferiorsattribute.h>
+
+#include <cachepolicy.h>
+#include <entitydisplayattribute.h>
+#include <akonadi/kmime/messageparts.h>
+#include <KLocalizedString>
+
+class TestRetrieveCollectionsTask : public ImapTestBase
+{
+    Q_OBJECT
+public:
+    TestRetrieveCollectionsTask(QObject *parent = Q_NULLPTR)
+        : ImapTestBase(parent), m_nextCollectionId(1)
+    {
+    }
+
+private Q_SLOTS:
+    void shouldListCollections_data()
+    {
+        QTest::addColumn<Akonadi::Collection::List>("expectedCollections");
+        QTest::addColumn< QList<QByteArray> >("scenario");
+        QTest::addColumn<QStringList>("callNames");
+        QTest::addColumn<bool>("isSubscriptionEnabled");
+        QTest::addColumn<bool>("isDisconnectedModeEnabled");
+        QTest::addColumn<int>("intervalCheckTime");
+        QTest::addColumn<QChar>("separator");
+
+        Akonadi::Collection collection;
+
+        Akonadi::Collection::List expectedCollections;
+        QList<QByteArray> scenario;
+        QStringList callNames;
+        bool isSubscriptionEnabled;
+        bool isDisconnectedModeEnabled;
+        int intervalCheckTime;
+
+        expectedCollections.clear();
+        expectedCollections << createRootCollection()
+                            << createCollection(QStringLiteral("/"), QStringLiteral("INBOX"))
+                            << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Calendar"))
+                            << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Calendar/Private"))
+                            << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Archives"));
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 LIST \"\" *"
+                 << "S: * LIST ( \\HasChildren ) / INBOX"
+                 << "S: * LIST ( \\HasChildren ) / INBOX/Calendar"
+                 << "S: * LIST ( ) / INBOX/Calendar/Private"
+                 << "S: * LIST ( ) / INBOX/Archives"
+                 << "S: A000003 OK list done";
+
+        callNames.clear();
+        callNames << QStringLiteral("setIdleCollection") << QStringLiteral("collectionsRetrieved");
+
+        isSubscriptionEnabled = false;
+        isDisconnectedModeEnabled = false;
+        intervalCheckTime = -1;
+
+        QTest::newRow("first listing, connected IMAP") << expectedCollections << scenario << callNames
+                << isSubscriptionEnabled << isDisconnectedModeEnabled << intervalCheckTime
+                << QChar('/');
+
+        expectedCollections.clear();
+        expectedCollections << createRootCollection(true, 5)
+                            << createCollection(QStringLiteral("/"), QStringLiteral("INBOX"))
+                            << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Calendar"))
+                            << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Calendar/Private"))
+                            << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Archives"));
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 LIST \"\" *"
+                 << "S: * LIST ( \\HasChildren ) / INBOX"
+                 << "S: * LIST ( \\HasChildren ) / INBOX/Calendar"
+                 << "S: * LIST ( ) / INBOX/Calendar/Private"
+                 << "S: * LIST ( ) / INBOX/Archives"
+                 << "S: A000003 OK list done";
+
+        callNames.clear();
+        callNames << QStringLiteral("setIdleCollection") << QStringLiteral("collectionsRetrieved");
+
+        isSubscriptionEnabled = false;
+        isDisconnectedModeEnabled = true;
+        intervalCheckTime = 5;
+
+        QTest::newRow("first listing, disconnected IMAP") << expectedCollections << scenario << callNames
+                << isSubscriptionEnabled << isDisconnectedModeEnabled << intervalCheckTime
+                << QChar('/');
+
+        expectedCollections.clear();
+        expectedCollections << createRootCollection(true, 5)
+                            << createCollection(QStringLiteral("/"), QStringLiteral("INBOX"))
+                            << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Archives"));
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 LIST \"\" *"
+                 << "S: * LIST ( \\HasChildren ) / INBOX"
+                 << "S: * LIST ( \\HasChildren ) / INBOX/"
+                 << "S: * LIST ( \\HasChildren ) / INBOX/Archives"
+                 << "S: A000003 OK list done";
+
+        callNames.clear();
+        callNames << QStringLiteral("setIdleCollection") << QStringLiteral("collectionsRetrieved");
+
+        isSubscriptionEnabled = false;
+        isDisconnectedModeEnabled = true;
+        intervalCheckTime = 5;
+
+        QTest::newRow("first listing, spurious INBOX/ (BR: 25342)") << expectedCollections << scenario << callNames
+                << isSubscriptionEnabled << isDisconnectedModeEnabled << intervalCheckTime
+                << QChar('/');
+
+        expectedCollections.clear();
+        expectedCollections << createRootCollection()
+                            << createCollection(QStringLiteral("/"), QStringLiteral("INBOX"))
+                            << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Calendar"), true)
+                            << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Calendar/Private"))
+                            << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Archives"));
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 LIST \"\" *"
+                 << "S: * LIST ( \\HasChildren ) / INBOX"
+                 << "S: * LIST ( ) / INBOX/Calendar/Private"
+                 << "S: * LIST ( ) / INBOX/Archives"
+                 << "S: A000003 OK list done";
+
+        callNames.clear();
+        callNames << QStringLiteral("setIdleCollection") << QStringLiteral("collectionsRetrieved");
+
+        isSubscriptionEnabled = false;
+        isDisconnectedModeEnabled = false;
+        intervalCheckTime = -1;
+
+        QTest::newRow("auto-insert missing nodes in the tree") << expectedCollections << scenario << callNames
+                << isSubscriptionEnabled << isDisconnectedModeEnabled << intervalCheckTime
+                << QChar('/');
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 LIST \"\" *"
+                 << "S: * LIST ( ) / INBOX/Archives"
+                 << "S: * LIST ( ) / INBOX/Calendar/Private"
+                 << "S: * LIST ( \\HasChildren ) / INBOX"
+                 << "S: A000003 OK list done";
+
+        callNames.clear();
+        callNames << QStringLiteral("setIdleCollection") << QStringLiteral("collectionsRetrieved");
+
+        isSubscriptionEnabled = false;
+        isDisconnectedModeEnabled = false;
+        intervalCheckTime = -1;
+
+        QTest::newRow("auto-insert missing nodes in the tree (reverse order)")
+                << expectedCollections << scenario << callNames
+                << isSubscriptionEnabled << isDisconnectedModeEnabled << intervalCheckTime
+                << QChar('/');
+
+        expectedCollections.clear();
+        expectedCollections << createRootCollection()
+                            << createCollection(QStringLiteral("/"), QStringLiteral("INBOX"))
+                            << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Calendar"))
+                            << createCollection(QStringLiteral("/"), QStringLiteral("INBOX/Calendar/Private"));
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 LIST \"\" *"
+                 << "S: * LIST ( ) / INBOX/Unsubscribed"
+                 << "S: * LIST ( ) / INBOX/Calendar"
+                 << "S: * LIST ( ) / INBOX/Calendar/Private"
+                 << "S: * LIST ( \\HasChildren ) / INBOX"
+                 << "S: A000003 OK list done"
+                 << "C: A000004 LSUB \"\" *"
+                 << "S: * LSUB ( \\HasChildren ) / INBOX"
+                 << "S: * LSUB ( ) / INBOX/SubscribedButNotExisting"
+                 << "S: * LSUB ( ) / INBOX/Calendar"
+                 << "S: * LSUB ( ) / INBOX/Calendar/Private"
+                 << "S: A000004 OK list done";
+
+        callNames.clear();
+        callNames << QStringLiteral("setIdleCollection") << QStringLiteral("collectionsRetrieved");
+
+        isSubscriptionEnabled = true;
+        isDisconnectedModeEnabled = false;
+        intervalCheckTime = -1;
+
+        QTest::newRow("subscription enabled") << expectedCollections << scenario << callNames
+                                              << isSubscriptionEnabled << isDisconnectedModeEnabled << intervalCheckTime
+                                              << QChar('/');
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 LIST \"\" *"
+                 << "S: * LIST ( ) / INBOX/Unsubscribed"
+                 << "S: * LIST ( ) / INBOX/Calendar"
+                 << "S: * LIST ( ) / INBOX/Calendar/Private"
+                 << "S: * LIST ( \\HasChildren ) / INBOX"
+                 << "S: A000003 OK list done"
+                 << "C: A000004 LSUB \"\" *"
+                 << "S: * LSUB ( \\HasChildren ) / Inbox"
+                 << "S: * LSUB ( ) / Inbox/SubscribedButNotExisting"
+                 << "S: * LSUB ( ) / Inbox/Calendar"
+                 << "S: * LSUB ( ) / Inbox/Calendar/Private"
+                 << "S: A000004 OK list done";
+
+        callNames.clear();
+        callNames << QStringLiteral("setIdleCollection") << QStringLiteral("collectionsRetrieved");
+
+        isSubscriptionEnabled = true;
+        isDisconnectedModeEnabled = false;
+        intervalCheckTime = -1;
+
+        QTest::newRow("subscription enabled, case insensitive inbox") << expectedCollections << scenario << callNames
+                << isSubscriptionEnabled << isDisconnectedModeEnabled << intervalCheckTime
+                << QChar('/');
+
+        expectedCollections.clear();
+        expectedCollections << createRootCollection()
+                            << createCollection(QStringLiteral("/"), QStringLiteral("INBOX"), false, true)
+                            << createCollection(QStringLiteral("/"), QStringLiteral("Archive"));
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 LIST \"\" *"
+                 << "S: * LIST ( \\Noinferiors ) / INBOX"
+                 << "S: * LIST ( ) / Archive"
+                 << "S: A000003 OK list done";
+
+        callNames.clear();
+        callNames << QStringLiteral("setIdleCollection") << QStringLiteral("collectionsRetrieved");
+
+        isSubscriptionEnabled = false;
+        isDisconnectedModeEnabled = false;
+        intervalCheckTime = -1;
+
+        QTest::newRow("Noinferiors") << expectedCollections << scenario << callNames
+                                     << isSubscriptionEnabled << isDisconnectedModeEnabled << intervalCheckTime
+                                     << QChar('/');
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 LIST \"\" *"
+                 << "S: * LIST ( ) . INBOX"
+                 << "S: * LIST ( ) . INBOX.Foo"
+                 << "S: * LIST ( ) . INBOX.Bar"
+                 << "S: A000003 OK list done";
+        callNames.clear();
+        callNames << QStringLiteral("setIdleCollection") << QStringLiteral("collectionsRetrieved");
+
+        expectedCollections.clear();
+        expectedCollections << createRootCollection()
+                            << createCollection(QStringLiteral("."), QStringLiteral("INBOX"))
+                            << createCollection(QStringLiteral("."), QStringLiteral("INBOX.Foo"))
+                            << createCollection(QStringLiteral("."), QStringLiteral("INBOX.Bar"));
+        isSubscriptionEnabled = false;
+        isDisconnectedModeEnabled = false;
+        intervalCheckTime = -1;
+
+        QTest::newRow("non-standard separators") << expectedCollections << scenario << callNames
+                << isSubscriptionEnabled << isDisconnectedModeEnabled << intervalCheckTime
+                << QChar('.');
+    }
+
+    void shouldListCollections()
+    {
+        QFETCH(Akonadi::Collection::List, expectedCollections);
+        QFETCH(QList<QByteArray>, scenario);
+        QFETCH(QStringList, callNames);
+        QFETCH(bool, isSubscriptionEnabled);
+        QFETCH(bool, isDisconnectedModeEnabled);
+        QFETCH(int, intervalCheckTime);
+        QFETCH(QChar, separator);
+
+        FakeServer server;
+        server.setScenario(scenario);
+        server.startAndWait();
+
+        SessionPool pool(1);
+
+        pool.setPasswordRequester(createDefaultRequester());
+        QVERIFY(pool.connect(createDefaultAccount()));
+        QVERIFY(waitForSignal(&pool, SIGNAL(connectDone(int,QString))));
+
+        DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState);
+        state->setResourceName(QStringLiteral("resource"));
+        state->setSubscriptionEnabled(isSubscriptionEnabled);
+        state->setDisconnectedModeEnabled(isDisconnectedModeEnabled);
+        state->setIntervalCheckTime(intervalCheckTime);
+
+        RetrieveCollectionsTask *task = new RetrieveCollectionsTask(state);
+        task->start(&pool);
+
+        Akonadi::Collection::List collections;
+
+        QTRY_COMPARE(state->calls().count(), callNames.size());
+        for (int i = 0; i < callNames.size(); i++) {
+            QString command = QString::fromUtf8(state->calls().at(i).first);
+            QVariant parameter = state->calls().at(i).second;
+
+            if (command == QLatin1String("cancelTask") && callNames[i] != QLatin1String("cancelTask")) {
+                qDebug() << "Got a cancel:" << parameter.toString();
+            }
+
+            QCOMPARE(command, callNames[i]);
+
+            if (command == QLatin1String("cancelTask")) {
+                QVERIFY(!parameter.toString().isEmpty());
+            } else if (command == QLatin1String("collectionsRetrieved")) {
+                collections += parameter.value<Akonadi::Collection::List>();
+            }
+        }
+
+        QCOMPARE(state->separatorCharacter(), separator);
+
+        QVERIFY(server.isAllScenarioDone());
+        compareCollectionLists(collections, expectedCollections);
+
+        server.quit();
+    }
+
+private:
+    qint64 m_nextCollectionId;
+
+    Akonadi::Collection createRootCollection(bool isDisconnectedImap = false, int intervalCheck = -1)
+    {
+        // Root
+        Akonadi::Collection collection = Akonadi::Collection(m_nextCollectionId++);
+        collection.setName(QStringLiteral("resource"));
+        collection.setRemoteId(QStringLiteral("root-id"));
+        collection.setContentMimeTypes(QStringList(Akonadi::Collection::mimeType()));
+        collection.setParentCollection(Akonadi::Collection::root());
+        collection.addAttribute(new NoSelectAttribute(true));
+
+        Akonadi::CachePolicy policy;
+        policy.setInheritFromParent(false);
+        policy.setSyncOnDemand(true);
+
+        if (isDisconnectedImap) {
+            policy.setLocalParts(QStringList() << Akonadi::MessagePart::Envelope
+                                 << Akonadi::MessagePart::Header
+                                 << Akonadi::MessagePart::Body);
+            policy.setCacheTimeout(-1);
+        } else {
+            policy.setLocalParts(QStringList() << Akonadi::MessagePart::Envelope
+                                 << Akonadi::MessagePart::Header);
+            policy.setCacheTimeout(60);
+        }
+
+        policy.setIntervalCheckTime(intervalCheck);
+
+        collection.setCachePolicy(policy);
+
+        return collection;
+    }
+
+    Akonadi::Collection createCollection(const QString &separator, const QString &path, bool isNoSelect = false, bool isNoInferiors = false)
+    {
+        // No path? That's the root of this resource then
+        if (path.isEmpty()) {
+            return createRootCollection();
+        }
+
+        QStringList pathParts = path.split(separator);
+
+        const QString pathPart = pathParts.takeLast();
+        const QString parentPath = pathParts.join(separator);
+
+        // Here we should likely reuse already produced collections if possible to be 100% accurate
+        // but in the tests we check only a limited amount of properties (namely remote id and name).
+        const Akonadi::Collection parentCollection = createCollection(separator, parentPath);
+
+        Akonadi::Collection collection(m_nextCollectionId++);
+        collection.setName(pathPart);
+        collection.setRemoteId(separator + pathPart);
+
+        collection.setParentCollection(parentCollection);
+        collection.setContentMimeTypes(QStringList() << QStringLiteral("message/rfc822") << Akonadi::Collection::mimeType());
+
+        // If the folder is the Inbox, make some special settings.
+        if (pathPart.compare(QLatin1String("INBOX"), Qt::CaseInsensitive) == 0) {
+            Akonadi::EntityDisplayAttribute *attr = new Akonadi::EntityDisplayAttribute;
+            attr->setDisplayName(i18n("Inbox"));
+            attr->setIconName(QStringLiteral("mail-folder-inbox"));
+            collection.addAttribute(attr);
+        }
+
+        // If the folder is the user top-level folder, mark it as well, even although it is not officially noted in the RFC
+        if ((pathPart.compare(QLatin1String("user"), Qt::CaseInsensitive) == 0) && isNoSelect) {
+            Akonadi::EntityDisplayAttribute *attr = new Akonadi::EntityDisplayAttribute;
+            attr->setDisplayName(i18n("Shared Folders"));
+            attr->setIconName(QStringLiteral("x-mail-distribution-list"));
+            collection.addAttribute(attr);
+        }
+
+        // If this folder is a noselect folder, make some special settings.
+        if (isNoSelect) {
+            collection.addAttribute(new NoSelectAttribute(true));
+            collection.setContentMimeTypes(QStringList() << Akonadi::Collection::mimeType());
+            collection.setRights(Akonadi::Collection::ReadOnly);
+        }
+
+        if (isNoInferiors) {
+            collection.addAttribute(new NoInferiorsAttribute(true));
+            collection.setRights(collection.rights() & ~Akonadi::Collection::CanCreateCollection);
+        }
+
+        return collection;
+    }
+
+    void compareCollectionLists(const Akonadi::Collection::List &resultList,
+                                const Akonadi::Collection::List &expectedList)
+    {
+        for (int i = 0; i < expectedList.size(); i++) {
+            Akonadi::Collection expected = expectedList[i];
+            bool found = false;
+
+            for (int j = 0; j < resultList.size(); j++) {
+                Akonadi::Collection result = resultList[j];
+
+                if (result.remoteId() == expected.remoteId()) {
+                    found = true;
+
+                    QVERIFY(!result.name().isEmpty());
+
+                    QCOMPARE(result.name(), expected.name());
+                    QCOMPARE(result.contentMimeTypes(), expected.contentMimeTypes());
+                    QCOMPARE(result.rights(), expected.rights());
+                    if (expected.parentCollection() == Akonadi::Collection::root()) {
+                        QCOMPARE(result.parentCollection(), expected.parentCollection());
+                    } else {
+                        QCOMPARE(result.parentCollection().remoteId(), expected.parentCollection().remoteId());
+                    }
+
+                    QCOMPARE(result.cachePolicy().inheritFromParent(), expected.cachePolicy().inheritFromParent());
+                    QCOMPARE(result.cachePolicy().syncOnDemand(), expected.cachePolicy().syncOnDemand());
+                    QCOMPARE(result.cachePolicy().localParts(), expected.cachePolicy().localParts());
+                    QCOMPARE(result.cachePolicy().cacheTimeout(), expected.cachePolicy().cacheTimeout());
+                    QCOMPARE(result.cachePolicy().intervalCheckTime(), expected.cachePolicy().intervalCheckTime());
+
+                    QCOMPARE(result.hasAttribute<NoSelectAttribute>(), expected.hasAttribute<NoSelectAttribute>());
+                    QCOMPARE(result.hasAttribute<Akonadi::EntityDisplayAttribute>(), expected.hasAttribute<Akonadi::EntityDisplayAttribute>());
+
+                    break;
+                }
+            }
+
+            QVERIFY2(found, QString::fromLatin1("%1 not found!").arg(expected.remoteId()).toUtf8().constData());
+        }
+
+        QCOMPARE(resultList.size(), expectedList.size());
+    }
+};
+
+QTEST_GUILESS_MAIN(TestRetrieveCollectionsTask)
+
+#include "testretrievecollectionstask.moc"
diff --git a/resources/imap/autotests/testretrieveitemstask.cpp b/resources/imap/autotests/testretrieveitemstask.cpp
new file mode 100644 (file)
index 0000000..ec38f44
--- /dev/null
@@ -0,0 +1,614 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "imaptestbase.h"
+
+#include "retrieveitemstask.h"
+#include "uidnextattribute.h"
+#include <highestmodseqattribute.h>
+#include <uidvalidityattribute.h>
+
+#include <cachepolicy.h>
+#include <collectionstatistics.h>
+#include <akonadi/kmime/messageparts.h>
+
+class TestRetrieveItemsTask : public ImapTestBase
+{
+    Q_OBJECT
+
+private Q_SLOTS:
+    void shouldIntrospectCollection_data()
+    {
+        QTest::addColumn<Akonadi::Collection>("collection");
+        QTest::addColumn< QList<QByteArray> >("scenario");
+        QTest::addColumn<QStringList>("callNames");
+
+        Akonadi::Collection collection;
+        QList<QByteArray> scenario;
+        QStringList callNames;
+
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection.attribute<UidValidityAttribute>(Akonadi::Collection::AddIfMissing)->setUidValidity(1149151135);
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 OK select done"
+                 << "C: A000004 EXPUNGE"
+                 << "S: A000004 OK expunge done"
+                 << "C: A000005 SELECT \"INBOX/Foo\""
+                 << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)"
+                 << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]"
+                 << "S: * 1 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: * OK [ UIDVALIDITY 1149151135  ]"
+                 << "S: * OK [ UIDNEXT 9  ]"
+                 << "S: A000005 OK select done"
+                 << "C: A000006 UID SEARCH UID 1:9"
+                 << "S: * SEARCH 1 2 3 4 5 6 7 8 9"
+                 << "S: A000006 OK search done"
+                 << "C: A000007 UID FETCH 1:9 (RFC822.SIZE INTERNALDATE "
+                 "BODY.PEEK[HEADER] "
+                 "FLAGS UID)"
+                 << "S: * 1 FETCH ( FLAGS (\\Seen) UID 7 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" "
+                 "RFC822.SIZE 75 BODY[HEADER] {69}\r\n"
+                 "From: Foo <foo@kde.org>\r\n"
+                 "To: Bar <bar@kde.org>\r\n"
+                 "Subject: Test Mail\r\n"
+                 "\r\n"
+                 " )"
+                 << "S: A000007 OK fetch done";
+
+        callNames.clear();
+        callNames << QStringLiteral("itemsRetrieved") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievalDone");
+
+        QTest::newRow("first listing, connected IMAP") << collection << scenario << callNames;
+
+        Akonadi::CachePolicy policy;
+        policy.setLocalParts(QStringList() << Akonadi::MessagePart::Envelope
+                             << Akonadi::MessagePart::Header
+                             << Akonadi::MessagePart::Body);
+
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection.attribute<UidValidityAttribute>(Akonadi::Collection::AddIfMissing)->setUidValidity(1149151135);
+        collection.setCachePolicy(policy);
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 OK select done"
+                 << "C: A000004 EXPUNGE"
+                 << "S: A000004 OK expunge done"
+                 << "C: A000005 SELECT \"INBOX/Foo\""
+                 << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)"
+                 << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]"
+                 << "S: * 1 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: * OK [ UIDVALIDITY 1149151135  ]"
+                 << "S: * OK [ UIDNEXT 9  ]"
+                 << "S: A000005 OK select done"
+                 << "C: A000006 UID SEARCH UID 1:9"
+                 << "S: * SEARCH 1 2 3 4 5 6 7 8 9"
+                 << "S: A000006 OK search done"
+                 << "C: A000007 UID FETCH 1:9 (RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID)"
+                 << "S: * 1 FETCH ( FLAGS (\\Seen) UID 7 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" "
+                 "RFC822.SIZE 75 BODY[] {75}\r\n"
+                 "From: Foo <foo@kde.org>\r\n"
+                 "To: Bar <bar@kde.org>\r\n"
+                 "Subject: Test Mail\r\n"
+                 "\r\n"
+                 "Test\r\n"
+                 " )"
+                 << "S: A000007 OK fetch done";
+
+        callNames.clear();
+        callNames << QStringLiteral("itemsRetrieved") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievalDone");
+
+        QTest::newRow("first listing, disconnected IMAP") << collection << scenario << callNames;
+
+        Akonadi::CollectionStatistics stats;
+        stats.setCount(1);
+
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection.attribute<UidValidityAttribute>(Akonadi::Collection::AddIfMissing)->setUidValidity(1149151135);
+        collection.attribute<UidNextAttribute>(Akonadi::Collection::AddIfMissing)->setUidNext(9);
+        collection.setCachePolicy(policy);
+        collection.setStatistics(stats);
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 OK select done"
+                 << "C: A000004 EXPUNGE"
+                 << "S: A000004 OK expunge done"
+                 << "C: A000005 SELECT \"INBOX/Foo\""
+                 << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)"
+                 << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]"
+                 << "S: * 1 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: * OK [ UIDVALIDITY 1149151135  ]"
+                 << "S: * OK [ UIDNEXT 9  ]"
+                 << "S: A000005 OK select done"
+                 << "C: A000006 UID SEARCH UID 1:9"
+                 << "S: * SEARCH 1 2 3 4 5 6 7 8 9"
+                 << "S: A000006 OK search done"
+                 << "C: A000007 UID FETCH 1:9 (FLAGS UID)"
+                 << "S: * 1 FETCH ( FLAGS (\\Seen) UID 7 )"
+                 << "S: A000007 OK fetch done";
+
+        callNames.clear();
+        callNames << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("itemsRetrievalDone");
+
+        //Disabled test since the flag sync is disabled if CONDSTORE is not supported
+//     QTest::newRow( "second listing, checking for flag changes" ) << collection << scenario << callNames;
+
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection.attribute<UidValidityAttribute>(Akonadi::Collection::AddIfMissing)->setUidValidity(1149151135);
+        collection.setCachePolicy(policy);
+        stats.setCount(1);
+        collection.setStatistics(stats);
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 OK select done"
+                 << "C: A000004 EXPUNGE"
+                 << "S: A000004 OK expunge done"
+                 << "C: A000005 SELECT \"INBOX/Foo\""
+                 << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)"
+                 << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]"
+                 << "S: * 0 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: * OK [ UIDVALIDITY 1149151135  ]"
+                 << "S: * OK [ UIDNEXT 9  ]"
+                 << "S: A000005 OK select done";
+
+        callNames.clear();
+        callNames << QStringLiteral("itemsRetrieved") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievalDone");
+
+        QTest::newRow("third listing, full sync, empty folder") << collection << scenario << callNames;
+
+        collection.attribute<UidNextAttribute>(Akonadi::Collection::AddIfMissing)->setUidNext(8);
+        stats.setCount(4);
+        collection.setStatistics(stats);
+        collection.attribute<HighestModSeqAttribute>(Akonadi::Collection::AddIfMissing)->setHighestModSeq(123456788);
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 OK select done"
+                 << "C: A000004 EXPUNGE"
+                 << "S: A000004 OK expunge done"
+                 << "C: A000005 SELECT \"INBOX/Foo\""
+                 << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)"
+                 << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]"
+                 << "S: * 5 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: * OK [ UIDVALIDITY 1149151135  ]"
+                 << "S: * OK [ UIDNEXT 9  ]"
+                 << "S: * OK [ HIGHESTMODSEQ 123456789 ]"
+                 << "S: A000005 OK select done"
+                 << "C: A000006 UID SEARCH UID 8:9"
+                 << "S: * SEARCH 8 9"
+                 << "S: A000006 OK search done"
+                 << "C: A000007 UID FETCH 8:9 (RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID)"
+                 << "S: * 5 FETCH ( FLAGS (\\Seen) UID 9 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" "
+                 "RFC822.SIZE 75 BODY[] {75}\r\n"
+                 "From: Foo <foo@kde.org>\r\n"
+                 "To: Bar <bar@kde.org>\r\n"
+                 "Subject: Test Mail\r\n"
+                 "\r\n"
+                 "Test\r\n"
+                 " )"
+                 << "S: A000007 OK fetch done"
+                 << "C: A000008 UID SEARCH UID 1:7"
+                 << "S: * SEARCH 1 2 3 4 5 6 7"
+                 << "S: A000008 OK search done"
+                 << "C: A000009 UID FETCH 1:7 (FLAGS UID)"
+                 << "S: * 1 FETCH"
+                 << "S: * 2 FETCH"
+                 << "S: * 3 FETCH"
+                 << "S: * 4 FETCH"
+                 << "S: A000009 OK fetch done";
+
+        callNames.clear();
+        callNames << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("itemsRetrievalDone");
+
+        //We know no messages have been removed, so we can do an incremental update
+        QTest::newRow("uidnext changed, fetch new messages incrementally") << collection << scenario << callNames;
+
+        collection.attribute<UidNextAttribute>(Akonadi::Collection::AddIfMissing)->setUidNext(8);
+        stats.setCount(5);
+        collection.setStatistics(stats);
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 OK select done"
+                 << "C: A000004 EXPUNGE"
+                 << "S: A000004 OK expunge done"
+                 << "C: A000005 SELECT \"INBOX/Foo\""
+                 << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)"
+                 << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]"
+                 << "S: * 5 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: * OK [ UIDVALIDITY 1149151135  ]"
+                 << "S: * OK [ UIDNEXT 9  ]"
+                 << "S: * OK [ HIGHESTMODSEQ 123456789 ]"
+                 << "S: A000005 OK select done"
+                 << "C: A000006 UID SEARCH UID 8:9"
+                 << "S: * SEARCH 8 9"
+                 << "S: A000006 OK search done"
+                 << "C: A000007 UID FETCH 8:9 (RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID)"
+                 << "S: * 4 FETCH ( FLAGS (\\Seen) UID 8 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" "
+                 "RFC822.SIZE 75 BODY[] {75}\r\n"
+                 "From: Foo <foo@kde.org>\r\n"
+                 "To: Bar <bar@kde.org>\r\n"
+                 "Subject: Test Mail\r\n"
+                 "\r\n"
+                 "Test\r\n"
+                 " )"
+                 << "S: * 5 FETCH ( FLAGS (\\Seen) UID 9 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" "
+                 "RFC822.SIZE 75 BODY[] {75}\r\n"
+                 "From: Foo <foo@kde.org>\r\n"
+                 "To: Bar <bar@kde.org>\r\n"
+                 "Subject: Test Mail\r\n"
+                 "\r\n"
+                 "Test\r\n"
+                 " )"
+                 << "S: A000007 OK fetch done"
+                 << "C: A000008 UID SEARCH UID 1:7"
+                 << "S: * SEARCH 1 2 3 4 5 6 7"
+                 << "S: A000008 OK search done"
+                 << "C: A000009 UID FETCH 1:7 (FLAGS UID)"
+                 << "S: * 1 FETCH"
+                 << "S: * 2 FETCH"
+                 << "S: * 3 FETCH"
+                 << "S: A000009 OK fetch done";
+
+        callNames.clear();
+        callNames << QStringLiteral("itemsRetrieved") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievalDone");
+
+        //A new message has been added and an old one removed, we can't do an incremental update
+        QTest::newRow("uidnext changed, fetch new messages and list flags") << collection << scenario << callNames;
+
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection.attribute<UidValidityAttribute>(Akonadi::Collection::AddIfMissing)->setUidValidity(1149151135);
+        collection.setCachePolicy(policy);
+        collection.attribute<UidNextAttribute>(Akonadi::Collection::AddIfMissing)->setUidNext(9);
+        collection.attribute<HighestModSeqAttribute>(Akonadi::Collection::AddIfMissing)->setHighestModSeq(123456789);
+        stats.setCount(5);
+        collection.setStatistics(stats);
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario(QList<QByteArray>() << "CONDSTORE")
+                 << "C: A000003 SELECT \"INBOX/Foo\" (CONDSTORE)"
+                 << "S: A000003 OK select done"
+                 << "C: A000004 EXPUNGE"
+                 << "S: A000004 OK expunge DONE"
+                 << "C: A000005 SELECT \"INBOX/Foo\" (CONDSTORE)"
+                 << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)"
+                 << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]"
+                 << "S: * 5 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: * OK [ UIDVALIDITY 1149151135 ]"
+                 << "S: * OK [ UIDNEXT 9 ]"
+                 << "S: * OK [ HIGHESTMODSEQ 123456789 ]"
+                 << "S: A000005 OK select done";
+        callNames.clear();
+        callNames << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("itemsRetrievalDone");
+
+        //No flags have changed
+        QTest::newRow("highestmodseq test") << collection << scenario << callNames;
+
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection.attribute<UidValidityAttribute>(Akonadi::Collection::AddIfMissing)->setUidValidity(1149151135);
+        collection.setCachePolicy(policy);
+        collection.attribute<UidNextAttribute>(Akonadi::Collection::AddIfMissing)->setUidNext(9);
+        collection.attribute<HighestModSeqAttribute>(Akonadi::Collection::AddIfMissing)->setHighestModSeq(123456788);
+        stats.setCount(5);
+        collection.setStatistics(stats);
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario(QList<QByteArray>() << "CONDSTORE")
+                 << "C: A000003 SELECT \"INBOX/Foo\" (CONDSTORE)"
+                 << "S: A000003 OK select done"
+                 << "C: A000004 EXPUNGE"
+                 << "S: A000004 OK expunge DONE"
+                 << "C: A000005 SELECT \"INBOX/Foo\" (CONDSTORE)"
+                 << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)"
+                 << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]"
+                 << "S: * 5 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: * OK [ UIDVALIDITY 1149151135 ]"
+                 << "S: * OK [ UIDNEXT 9 ]"
+                 << "S: * OK [ HIGHESTMODSEQ 123456789 ]"
+                 << "S: A000005 OK select done"
+                 << "C: A000006 UID FETCH 1:9 (FLAGS UID) (CHANGEDSINCE 123456788)"
+                 << "S: * 5 FETCH ( UID 8 FLAGS () )"
+                 << "S: A000006 OK fetch done";
+        callNames.clear();
+        callNames << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("itemsRetrievalDone");
+
+        //fetch only changed flags
+        QTest::newRow("changedsince test") << collection << scenario << callNames;
+
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection.setCachePolicy(policy);
+        collection.attribute<UidValidityAttribute>(Akonadi::Collection::AddIfMissing)->setUidValidity(1149151135);
+        collection.attribute<UidNextAttribute>(Akonadi::Collection::AddIfMissing)->setUidNext(9);
+        collection.attribute<HighestModSeqAttribute>(Akonadi::Collection::AddIfMissing)->setHighestModSeq(123456788);
+        stats.setCount(5);
+        collection.setStatistics(stats);
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario(QList<QByteArray>() << "XYMHIGHESTMODSEQ")
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 OK select done"
+                 << "C: A000004 EXPUNGE"
+                 << "S: A000004 OK expunge DONE"
+                 << "C: A000005 SELECT \"INBOX/Foo\""
+                 << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)"
+                 << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]"
+                 << "S: * 5 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: * OK [ UIDVALIDITY 1149151135 ]"
+                 << "S: * OK [ UIDNEXT 9 ]"
+                 << "S: * OK [ HIGHESTMODSEQ 123456789 ]"
+                 << "S: A000005 OK select done";
+        //Disabled since the flag sync is disabled if CONDSTORE is not supported
+//              << "C: A000006 UID SEARCH UID 1:9"
+//              << "S: * SEARCH 1 2 3 4 5 6 7 8 9"
+//              << "S: A000006 OK search done"
+//              << "C: A000007 UID FETCH 1:9 (FLAGS UID)"
+//              << "S: * 5 FETCH ( UID 8 FLAGS () )"
+//              << "S: A000007 OK fetch done";
+        callNames.clear();
+
+        //Disabled since the flag sync is disabled if CONDSTORE is not supported
+        callNames << /*"itemsRetrievedIncremental" << */QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("itemsRetrievalDone");
+
+        //Don't rely on yahoos highestmodseq implementation
+        QTest::newRow("yahoo highestmodseq test") << collection << scenario << callNames;
+
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection.attribute<UidNextAttribute>(Akonadi::Collection::AddIfMissing)->setUidNext(9);
+        collection.attribute<UidValidityAttribute>(Akonadi::Collection::AddIfMissing)->setUidValidity(3);
+        collection.setCachePolicy(policy);
+        stats.setCount(1);
+        collection.setStatistics(stats);
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 OK select done"
+                 << "C: A000004 EXPUNGE"
+                 << "S: A000004 OK expunge done"
+                 << "C: A000005 SELECT \"INBOX/Foo\""
+                 << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)"
+                 << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]"
+                 << "S: * 1 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: * OK [ UIDVALIDITY 1149151135  ]"
+                 << "S: * OK [ UIDNEXT 9  ]"
+                 << "S: A000005 OK select done"
+                 << "C: A000006 UID SEARCH UID 1:9"
+                 << "S: * SEARCH 1 2 3 4 5 6 7 8 9"
+                 << "S: A000006 OK search done"
+                 << "C: A000007 UID FETCH 1:9 (RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID)"
+                 << "S: * 1 FETCH ( FLAGS (\\Seen) UID 2321 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" "
+                 "RFC822.SIZE 75 BODY[] {75}\r\n"
+                 "From: Foo <foo@kde.org>\r\n"
+                 "To: Bar <bar@kde.org>\r\n"
+                 "Subject: Test Mail\r\n"
+                 "\r\n"
+                 "Test\r\n"
+                 " )"
+                 << "S: A000007 OK fetch done";
+
+        callNames.clear();
+        callNames << QStringLiteral("itemsRetrieved") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievalDone");
+
+        QTest::newRow("uidvalidity changed") << collection << scenario << callNames;
+
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection.attribute<UidNextAttribute>(Akonadi::Collection::AddIfMissing)->setUidNext(105);
+        collection.attribute<UidValidityAttribute>(Akonadi::Collection::AddIfMissing)->setUidValidity(1149151135);
+        collection.setCachePolicy(policy);
+        stats.setCount(104);
+        collection.setStatistics(stats);
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 OK select done"
+                 << "C: A000004 EXPUNGE"
+                 << "S: A000004 OK expunge done"
+                 << "C: A000005 SELECT \"INBOX/Foo\""
+                 << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)"
+                 << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]"
+                 << "S: * 119 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: * OK [ UIDVALIDITY 1149151135  ]"
+                 << "S: * OK [ UIDNEXT 120  ]"
+                 << "S: A000005 OK select done"
+                 << "C: A000006 UID SEARCH UID 105:120"
+                 //We asked for until 120 but only 119 is available (120 is uidnext)
+                 << "S: * SEARCH 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119"
+                 << "S: A000006 OK search done"
+                 << "C: A000007 UID FETCH 105:114 (RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID)"
+                 << "S: * 1 FETCH ( FLAGS (\\Seen) UID 105 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" "
+                 "RFC822.SIZE 75 BODY[] {75}\r\n"
+                 "From: Foo <foo@kde.org>\r\n"
+                 "To: Bar <bar@kde.org>\r\n"
+                 "Subject: Test Mail\r\n"
+                 "\r\n"
+                 "Test\r\n"
+                 " )"
+                 //9 more would follow but are excluded for clarity
+                 << "S: A000007 OK fetch done"
+                 << "C: A000008 UID FETCH 115:119 (RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID)"
+                 << "S: * 1 FETCH ( FLAGS (\\Seen) UID 115 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" "
+                 "RFC822.SIZE 75 BODY[] {75}\r\n"
+                 "From: Foo <foo@kde.org>\r\n"
+                 "To: Bar <bar@kde.org>\r\n"
+                 "Subject: Test Mail\r\n"
+                 "\r\n"
+                 "Test\r\n"
+                 " )"
+                 //4 more would follow but are excluded for clarity
+                 << "S: A000008 OK fetch done"
+                 << "C: A000009 UID SEARCH UID 1:104"
+                 << "S: * SEARCH 1 2 99 100"
+                 << "S: A000009 OK search done"
+                 << "C: A000010 UID FETCH 1:2,99:100 (FLAGS UID)"
+                 << "S: * 1 FETCH ( FLAGS (\\Seen) UID 1 )"
+                 //3 more would follow but are excluded for clarity
+                 << "S: A000010 OK fetch done";
+
+        callNames.clear();
+        callNames << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("itemsRetrievalDone");
+
+        QTest::newRow("test batch processing") << collection << scenario << callNames;
+
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection.attribute<UidValidityAttribute>(Akonadi::Collection::AddIfMissing)->setUidValidity(1149151135);
+        collection.setCachePolicy(policy);
+        collection.attribute<UidNextAttribute>(Akonadi::Collection::AddIfMissing)->setUidNext(9);
+        collection.attribute<HighestModSeqAttribute>(Akonadi::Collection::AddIfMissing)->setHighestModSeq(123456789);
+        stats.setCount(5);
+        collection.setStatistics(stats);
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario(QList<QByteArray>() << "CONDSTORE")
+                 << "C: A000003 SELECT \"INBOX/Foo\" (CONDSTORE)"
+                 << "S: A000003 OK select done"
+                 << "C: A000004 EXPUNGE"
+                 << "S: A000004 OK expunge DONE"
+                 << "C: A000005 SELECT \"INBOX/Foo\" (CONDSTORE)"
+                 << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)"
+                 << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]"
+                 << "S: * 4 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: * OK [ UIDVALIDITY 1149151135 ]"
+                 << "S: * OK [ UIDNEXT 9 ]"
+                 << "S: * OK [ HIGHESTMODSEQ 123456789 ]"
+                 << "S: A000005 OK select done"
+                 << "C: A000006 UID SEARCH UID 1:9"
+                 << "S: * SEARCH 1 2 3 4"
+                 << "S: A000006 OK search done"
+                 << "C: A000007 UID FETCH 1:4 (FLAGS UID)"
+                 << "S: * 1 FETCH ( FLAGS (\\Seen) UID 1 )"
+                 << "S: * 2 FETCH ( FLAGS (\\Seen) UID 2 )"
+                 << "S: * 3 FETCH ( FLAGS (\\Seen) UID 3 )"
+                 << "S: * 4 FETCH ( FLAGS (\\Seen) UID 4 )"
+                 << "S: A000007 OK fetch done";
+        callNames.clear();
+        callNames << QStringLiteral("itemsRetrieved") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievalDone");
+
+        //fetch only changed flags
+        QTest::newRow("remote message deleted") << collection << scenario << callNames;
+
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        collection.attribute<UidValidityAttribute>(Akonadi::Collection::AddIfMissing)->setUidValidity(1149151135);
+        collection.setCachePolicy(policy);
+        collection.attribute<UidNextAttribute>(Akonadi::Collection::AddIfMissing)->setUidNext(-1);
+        collection.attribute<HighestModSeqAttribute>(Akonadi::Collection::AddIfMissing)->setHighestModSeq(123456789);
+        stats.setCount(0);
+        collection.setStatistics(stats);
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 OK select done"
+                 << "C: A000004 EXPUNGE"
+                 << "S: A000004 OK expunge done"
+                 << "C: A000005 SELECT \"INBOX/Foo\""
+                 << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)"
+                 << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]"
+                 << "S: * 1 EXISTS"
+                 << "S: * 0 RECENT"
+                 << "S: * OK [ UIDVALIDITY 1149151135  ]"
+                 << "S: A000005 OK select done"
+                 << "C: A000006 STATUS \"INBOX/Foo\" (UIDNEXT)"
+                 << "S: * STATUS \"INBOX/Foo\" (UIDNEXT 10)"
+                 << "S: A000006 OK status done"
+                 << "C: A000007 UID SEARCH UID 1:10"
+                 << "S: * SEARCH 1 2 3 4 5 6 7 8 9"
+                 << "S: A000007 OK search done"
+                 << "C: A000008 UID FETCH 1:9 (RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID)"
+                 << "S: * 1 FETCH ( FLAGS (\\Seen) UID 2321 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" "
+                 "RFC822.SIZE 75 BODY[] {75}\r\n"
+                 "From: Foo <foo@kde.org>\r\n"
+                 "To: Bar <bar@kde.org>\r\n"
+                 "Subject: Test Mail\r\n"
+                 "\r\n"
+                 "Test\r\n"
+                 " )"
+                 << "S: A000008 OK fetch done";
+
+        callNames.clear();
+        callNames << QStringLiteral("itemsRetrieved") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievalDone");
+
+        QTest::newRow("missing uidnext") << collection << scenario << callNames;
+
+    }
+
+    void shouldIntrospectCollection()
+    {
+        QFETCH(Akonadi::Collection, collection);
+        QFETCH(QList<QByteArray>, scenario);
+        QFETCH(QStringList, callNames);
+
+        FakeServer server;
+        server.setScenario(scenario);
+        server.startAndWait();
+
+        SessionPool pool(1);
+
+        pool.setPasswordRequester(createDefaultRequester());
+        QVERIFY(pool.connect(createDefaultAccount()));
+        QVERIFY(waitForSignal(&pool, SIGNAL(connectDone(int,QString))));
+
+        DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState);
+        state->setServerCapabilities(pool.serverCapabilities());
+        state->setCollection(collection);
+
+        RetrieveItemsTask *task = new RetrieveItemsTask(state);
+        task->setFetchMissingItemBodies(false);
+        task->start(&pool);
+
+        QTRY_COMPARE(state->calls().count(), callNames.size());
+        qDebug() << state->calls();
+        for (int i = 0; i < callNames.size(); i++) {
+            QString command = QString::fromUtf8(state->calls().at(i).first);
+            QVariant parameter = state->calls().at(i).second;
+
+            if (command == QLatin1String("cancelTask") && callNames[i] != QLatin1String("cancelTask")) {
+                qDebug() << "Got a cancel:" << parameter.toString();
+            }
+
+            QCOMPARE(command, callNames[i]);
+
+            if (command == QLatin1String("cancelTask")) {
+                QVERIFY(!parameter.toString().isEmpty());
+            }
+        }
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+};
+
+QTEST_GUILESS_MAIN(TestRetrieveItemsTask)
+
+#include "testretrieveitemstask.moc"
diff --git a/resources/imap/autotests/testretrieveitemtask.cpp b/resources/imap/autotests/testretrieveitemtask.cpp
new file mode 100644 (file)
index 0000000..c9a26e4
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "imaptestbase.h"
+
+#include "retrieveitemtask.h"
+
+class TestRetrieveItemTask : public ImapTestBase
+{
+    Q_OBJECT
+
+private Q_SLOTS:
+    void shouldFetchMessage_data()
+    {
+        QTest::addColumn<Akonadi::Item>("item");
+        QTest::addColumn<QString>("message");
+        QTest::addColumn< QList<QByteArray> >("scenario");
+        QTest::addColumn<QString>("callName");
+
+        Akonadi::Collection collection;
+        Akonadi::Item item;
+        QString message;
+        QList<QByteArray> scenario;
+
+        collection = createCollectionChain(QStringLiteral("/INBOX/Foo"));
+        item = Akonadi::Item(2);
+        item.setParentCollection(collection);
+        item.setRemoteId(QStringLiteral("42"));
+
+        message = QStringLiteral("From: ervin\nTo: someone\nSubject: foo\n\nSpeechless...");
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 OK select done"
+                 << "C: A000004 UID FETCH 42 (BODY.PEEK[] UID)"
+                 << "S: * 10 FETCH (UID 42 BODY[] \"From: ervin\nTo: someone\nSubject: foo\n\nSpeechless...\")"
+                 << "S: A000004 OK fetch done";
+        QTest::newRow("normal case") << item << message << scenario << "itemRetrieved";
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 NO select fail";
+        QTest::newRow("select fail") << item << message << scenario << "cancelTask";
+
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"INBOX/Foo\""
+                 << "S: A000003 OK select done"
+                 << "C: A000004 UID FETCH 42 (BODY.PEEK[] UID)"
+                 << "S: A000004 NO fetch failed";
+        QTest::newRow("fetch fail") << item << message << scenario << "cancelTask";
+    }
+
+    void shouldFetchMessage()
+    {
+        QFETCH(Akonadi::Item, item);
+        QFETCH(QString, message);
+        QFETCH(QList<QByteArray>, scenario);
+        QFETCH(QString, callName);
+
+        FakeServer server;
+        server.setScenario(scenario);
+        server.startAndWait();
+
+        SessionPool pool(1);
+
+        pool.setPasswordRequester(createDefaultRequester());
+        QVERIFY(pool.connect(createDefaultAccount()));
+        QVERIFY(waitForSignal(&pool, SIGNAL(connectDone(int,QString))));
+
+        DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState);
+        state->setItem(item);
+        RetrieveItemTask *task = new RetrieveItemTask(state);
+        task->start(&pool);
+
+        QTRY_COMPARE(state->calls().count(), 1);
+
+        QString command = QString::fromUtf8(state->calls().first().first);
+        if (command == QLatin1String("cancelTask") && callName != QLatin1String("cancelTask")) {
+            qDebug() << "Got a cancel:" << state->calls().first().second.toString();
+        }
+        QCOMPARE(command, callName);
+
+        QVariant parameter = state->calls().first().second;
+
+        if (callName == QLatin1String("itemRetrieved")) {
+            QCOMPARE(parameter.value<Akonadi::Item>().id(), item.id());
+            QCOMPARE(parameter.value<Akonadi::Item>().remoteId(), item.remoteId());
+
+            QString payload = parameter.value<Akonadi::Item>().payload<KMime::Message::Ptr>()->encodedContent();
+
+            QCOMPARE(payload, message);
+
+        } else if (callName == QLatin1String("cancelTask")) {
+            QVERIFY(!parameter.toString().isEmpty());
+        } else {
+            QFAIL(QString("Unexpected call type: %1").arg(callName).toUtf8().constData());
+        }
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+};
+
+QTEST_GUILESS_MAIN(TestRetrieveItemTask)
+
+#include "testretrieveitemtask.moc"
diff --git a/resources/imap/autotests/testsessionpool.cpp b/resources/imap/autotests/testsessionpool.cpp
new file mode 100644 (file)
index 0000000..4ec48dc
--- /dev/null
@@ -0,0 +1,780 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "imaptestbase.h"
+#include <QSignalSpy>
+#include <kimap/capabilitiesjob.h>
+
+class TestSessionPool : public ImapTestBase
+{
+    Q_OBJECT
+
+private Q_SLOTS:
+    void shouldPrepareFirstSessionOnConnect_data()
+    {
+        QTest::addColumn<ImapAccount *>("account");
+        QTest::addColumn<DummyPasswordRequester *>("requester");
+        QTest::addColumn< QList<QByteArray> >("scenario");
+        QTest::addColumn<QString>("password");
+        QTest::addColumn<int>("errorCode");
+        QTest::addColumn<QStringList>("capabilities");
+
+        ImapAccount *account = 0;
+        DummyPasswordRequester *requester = 0;
+        QList<QByteArray> scenario;
+        QString password;
+        QStringList capabilities;
+
+        account = createDefaultAccount();
+        requester = createDefaultRequester();
+        scenario.clear();
+        scenario << FakeServer::greeting()
+                 << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""
+                 << "S: A000001 OK User Logged in"
+                 << "C: A000002 CAPABILITY"
+                 << "S: * CAPABILITY IMAP4 IMAP4rev1 NAMESPACE UIDPLUS IDLE"
+                 << "S: A000002 OK Completed"
+                 << "C: A000003 NAMESPACE"
+                 << "S: * NAMESPACE ( (\"INBOX/\" \"/\") ) ( (\"user/\" \"/\") ) ( (\"\" \"/\") )"
+                 << "S: A000003 OK Completed";
+        password = QStringLiteral("foobar");
+        int errorCode = SessionPool::NoError;
+        capabilities.clear();
+        capabilities << QStringLiteral("IMAP4") << QStringLiteral("IMAP4REV1") << QStringLiteral("NAMESPACE") << QStringLiteral("UIDPLUS") << QStringLiteral("IDLE");
+        QTest::newRow("normal case") << account << requester << scenario
+                                     << password << errorCode << capabilities;
+
+        account = createDefaultAccount();
+        requester = createDefaultRequester();
+        scenario.clear();
+        scenario << FakeServer::greeting()
+                 << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""
+                 << "S: A000001 OK User Logged in"
+                 << "C: A000002 CAPABILITY"
+                 << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE"
+                 << "S: A000002 OK Completed";
+        password = QStringLiteral("foobar");
+        errorCode = SessionPool::NoError;
+        capabilities.clear();
+        capabilities << QStringLiteral("IMAP4") << QStringLiteral("IMAP4REV1") << QStringLiteral("UIDPLUS") << QStringLiteral("IDLE");
+        QTest::newRow("no NAMESPACE support") << account << requester << scenario
+                                              << password << errorCode << capabilities;
+
+        account = createDefaultAccount();
+        requester = createDefaultRequester();
+        scenario.clear();
+        scenario << FakeServer::greeting()
+                 << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""
+                 << "S: A000001 OK User Logged in"
+                 << "C: A000002 CAPABILITY"
+                 << "S: * CAPABILITY IMAP4 IDLE"
+                 << "S: A000002 OK Completed"
+                 << "C: A000003 LOGOUT";
+        password = QStringLiteral("foobar");
+        errorCode = SessionPool::IncompatibleServerError;
+        capabilities.clear();
+        QTest::newRow("incompatible server") << account << requester << scenario
+                                             << password << errorCode << capabilities;
+
+        QList<DummyPasswordRequester::RequestType> requests;
+        QList<DummyPasswordRequester::ResultType> results;
+
+        account = createDefaultAccount();
+        requester = createDefaultRequester();
+        requests.clear();
+        results.clear();
+        requests << DummyPasswordRequester::StandardRequest << DummyPasswordRequester::WrongPasswordRequest;
+        results << DummyPasswordRequester::PasswordRetrieved << DummyPasswordRequester::UserRejected;
+        requester->setScenario(requests, results);
+        scenario.clear();
+        scenario << FakeServer::greeting()
+                 << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""
+                 << "S: A000001 NO Login failed"
+                 << "C: A000002 LOGOUT";
+        password = QStringLiteral("foobar");
+        errorCode = SessionPool::LoginFailError;
+        capabilities.clear();
+        QTest::newRow("login fail, user reject password entry") << account << requester << scenario
+                << password << errorCode << capabilities;
+
+        account = createDefaultAccount();
+        requester = createDefaultRequester();
+        requests.clear();
+        results.clear();
+        requests << DummyPasswordRequester::StandardRequest << DummyPasswordRequester::WrongPasswordRequest;
+        results << DummyPasswordRequester::PasswordRetrieved << DummyPasswordRequester::PasswordRetrieved;
+        requester->setScenario(requests, results);
+        scenario.clear();
+        scenario << FakeServer::greeting()
+                 << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""
+                 << "S: A000001 NO Login failed"
+                 << "C: A000002 LOGIN \"test@kdab.com\" \"foobar\""
+                 << "S: A000002 OK Login succeeded"
+                 << "C: A000003 CAPABILITY"
+                 << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE"
+                 << "S: A000003 OK Completed";
+        password = QStringLiteral("foobar");
+        errorCode = SessionPool::NoError;
+        capabilities.clear();
+        capabilities << QStringLiteral("IMAP4") << QStringLiteral("IMAP4REV1") << QStringLiteral("UIDPLUS") << QStringLiteral("IDLE");
+        QTest::newRow("login fail, user provide new password") << account << requester << scenario
+                << password << errorCode << capabilities;
+
+        account = createDefaultAccount();
+        requester = createDefaultRequester();
+        requests.clear();
+        results.clear();
+        requests << DummyPasswordRequester::StandardRequest << DummyPasswordRequester::WrongPasswordRequest;
+        results << DummyPasswordRequester::PasswordRetrieved << DummyPasswordRequester::EmptyPasswordEntered;
+        requester->setScenario(requests, results);
+        scenario.clear();
+        scenario << FakeServer::greeting()
+                 << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""
+                 << "S: A000001 NO Login failed"
+                 << "C: A000002 LOGOUT";
+        password = QStringLiteral("foobar");
+        errorCode = SessionPool::LoginFailError;
+        capabilities.clear();
+        QTest::newRow("login fail, user provided empty password") << account << requester << scenario
+                << password << errorCode << capabilities;
+
+        account = createDefaultAccount();
+        requester = createDefaultRequester();
+        requests.clear();
+        results.clear();
+        requests << DummyPasswordRequester::StandardRequest << DummyPasswordRequester::WrongPasswordRequest;
+        results << DummyPasswordRequester::PasswordRetrieved << DummyPasswordRequester::ReconnectNeeded;
+        requester->setScenario(requests, results);
+        scenario.clear();
+        scenario << FakeServer::greeting()
+                 << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""
+                 << "S: A000001 NO Login failed"
+                 << "C: A000002 LOGOUT";
+        password = QStringLiteral("foobar");
+        errorCode = SessionPool::ReconnectNeededError;
+        capabilities.clear();
+        QTest::newRow("login fail, user change the settings") << account << requester << scenario
+                << password << errorCode << capabilities;
+    }
+
+    void shouldPrepareFirstSessionOnConnect()
+    {
+        QFETCH(ImapAccount *, account);
+        QFETCH(DummyPasswordRequester *, requester);
+        QFETCH(QList<QByteArray>, scenario);
+        QFETCH(QString, password);
+        QFETCH(int, errorCode);
+        QFETCH(QStringList, capabilities);
+
+        QSignalSpy requesterSpy(requester, SIGNAL(done(int,QString)));
+
+        FakeServer server;
+        server.setScenario(scenario);
+        server.startAndWait();
+
+        SessionPool pool(2);
+
+        QVERIFY(!pool.isConnected());
+
+        QSignalSpy poolSpy(&pool, SIGNAL(connectDone(int,QString)));
+
+        pool.setPasswordRequester(requester);
+        QVERIFY(pool.connect(account));
+
+        QTest::qWait(200);
+        QVERIFY(requesterSpy.count() > 0);
+        if (requesterSpy.count() == 1) {
+            QCOMPARE(requesterSpy.at(0).at(0).toInt(), 0);
+            QCOMPARE(requesterSpy.at(0).at(1).toString(), password);
+        }
+
+        QCOMPARE(poolSpy.count(), 1);
+        QCOMPARE(poolSpy.at(0).at(0).toInt(), errorCode);
+        if (errorCode == SessionPool::NoError) {
+            QVERIFY(pool.isConnected());
+        } else {
+            QVERIFY(!pool.isConnected());
+        }
+
+        QCOMPARE(pool.serverCapabilities(), capabilities);
+        QVERIFY(pool.serverNamespaces().isEmpty());
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+
+    void shouldManageSeveralSessions()
+    {
+        FakeServer server;
+        server.addScenario(QList<QByteArray>()
+                           << FakeServer::greeting()
+                           << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""
+                           << "S: A000001 OK User Logged in"
+                           << "C: A000002 CAPABILITY"
+                           << "S: * CAPABILITY IMAP4 IMAP4rev1 NAMESPACE UIDPLUS IDLE"
+                           << "S: A000002 OK Completed"
+                           << "C: A000003 NAMESPACE"
+                           << "S: * NAMESPACE ( (\"INBOX/\" \"/\") ) ( (\"user/\" \"/\") ) ( (\"\" \"/\") )"
+                           << "S: A000003 OK Completed"
+                          );
+
+        server.addScenario(QList<QByteArray>()
+                           << FakeServer::greeting()
+                           << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""
+                           << "S: A000001 OK User Logged in"
+                          );
+
+        server.startAndWait();
+
+        ImapAccount *account = createDefaultAccount();
+        DummyPasswordRequester *requester = createDefaultRequester();
+
+        QSignalSpy requesterSpy(requester, SIGNAL(done(int,QString)));
+
+        SessionPool pool(2);
+        pool.setPasswordRequester(requester);
+
+        QSignalSpy connectSpy(&pool, SIGNAL(connectDone(int,QString)));
+        QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session*,int,QString)));
+
+        // Before connect we can't get any session
+        qint64 requestId = pool.requestSession();
+        QCOMPARE(requestId, qint64(-1));
+
+        // Initial connect should trigger only a password request and a connect
+        QVERIFY(pool.connect(account));
+        QTest::qWait(100);
+        QCOMPARE(requesterSpy.count(), 1);
+        QCOMPARE(connectSpy.count(), 1);
+        QCOMPARE(sessionSpy.count(), 0);
+
+        // Requesting a first session shouldn't create a new one,
+        // only sessionRequestDone is emitted right away
+        requestId = pool.requestSession();
+        QCOMPARE(requestId, qint64(1));
+        QTest::qWait(100);
+        QCOMPARE(requesterSpy.count(),  1);
+        QCOMPARE(connectSpy.count(), 1);
+        QCOMPARE(sessionSpy.count(), 1);
+
+        QCOMPARE(sessionSpy.at(0).at(0).toLongLong(), requestId);
+        QVERIFY(sessionSpy.at(0).at(1).value<KIMAP::Session *>() != 0);
+        QCOMPARE(sessionSpy.at(0).at(2).toInt(), 0);
+        QCOMPARE(sessionSpy.at(0).at(3).toString(), QString());
+
+        // Requesting an extra session should create a new one
+        // So for instance password will be requested
+        requestId = pool.requestSession();
+        QCOMPARE(requestId, qint64(2));
+        QTest::qWait(100);
+        QCOMPARE(requesterSpy.count(),  2);
+        QCOMPARE(connectSpy.count(), 1);
+        QCOMPARE(sessionSpy.count(), 2);
+
+        QCOMPARE(sessionSpy.at(1).at(0).toLongLong(), requestId);
+        QVERIFY(sessionSpy.at(1).at(1).value<KIMAP::Session *>() != 0);
+        // Should be different sessions...
+        QVERIFY(sessionSpy.at(0).at(1).value<KIMAP::Session *>() != sessionSpy.at(1).at(1).value<KIMAP::Session *>());
+        QCOMPARE(sessionSpy.at(1).at(2).toInt(), 0);
+        QCOMPARE(sessionSpy.at(1).at(3).toString(), QString());
+
+        // Requesting yet another session should fail as we reached the
+        // maximum pool size, and they're all reserved
+        requestId = pool.requestSession();
+        QCOMPARE(requestId, qint64(3));
+        QTest::qWait(100);
+        QCOMPARE(requesterSpy.count(),  2);
+        QCOMPARE(connectSpy.count(), 1);
+        QCOMPARE(sessionSpy.count(), 3);
+
+        QCOMPARE(sessionSpy.at(2).at(0).toLongLong(), requestId);
+        QVERIFY(sessionSpy.at(2).at(1).value<KIMAP::Session *>() == 0);
+        QCOMPARE(sessionSpy.at(2).at(2).toInt(), (int)SessionPool::NoAvailableSessionError);
+        QVERIFY(!sessionSpy.at(2).at(3).toString().isEmpty());
+
+        // OTOH, if we release one now, and then request another one
+        // it should succeed without even creating a new session
+        KIMAP::Session *session = sessionSpy.at(0).at(1).value<KIMAP::Session *>();
+        pool.releaseSession(session);
+        requestId = pool.requestSession();
+        QCOMPARE(requestId, qint64(4));
+        QTest::qWait(100);
+        QCOMPARE(requesterSpy.count(), 2);
+        QCOMPARE(connectSpy.count(), 1);
+        QCOMPARE(sessionSpy.count(), 4);
+
+        QCOMPARE(sessionSpy.at(3).at(0).toLongLong(), requestId);
+        // Only one session was available, so that should be the one we get gack
+        QVERIFY(sessionSpy.at(3).at(1).value<KIMAP::Session *>() == session);
+        QCOMPARE(sessionSpy.at(3).at(2).toInt(), 0);
+        QCOMPARE(sessionSpy.at(3).at(3).toString(), QString());
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+
+    void shouldNotifyConnectionLost()
+    {
+        FakeServer server;
+        server.addScenario(QList<QByteArray>()
+                           << FakeServer::greeting()
+                           << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""
+                           << "S: A000001 OK User Logged in"
+                           << "C: A000002 CAPABILITY"
+                           << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE"
+                           << "S: A000002 OK Completed"
+                           << "C: A000003 CAPABILITY"
+                           << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE"
+                           << "X"
+                          );
+
+        server.startAndWait();
+
+        ImapAccount *account = createDefaultAccount();
+        DummyPasswordRequester *requester = createDefaultRequester();
+
+        SessionPool pool(1);
+        pool.setPasswordRequester(requester);
+
+        QSignalSpy connectSpy(&pool, SIGNAL(connectDone(int,QString)));
+        QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session*,int,QString)));
+        QSignalSpy lostSpy(&pool, SIGNAL(connectionLost(KIMAP::Session*)));
+
+        // Initial connect should trigger only a password request and a connect
+        QVERIFY(pool.connect(account));
+        QTest::qWait(100);
+        QCOMPARE(connectSpy.count(), 1);
+        QCOMPARE(sessionSpy.count(), 0);
+
+        qint64 requestId = pool.requestSession();
+        QTest::qWait(100);
+        QCOMPARE(sessionSpy.count(), 1);
+
+        QCOMPARE(sessionSpy.at(0).at(0).toLongLong(), requestId);
+        KIMAP::Session *s = sessionSpy.at(0).at(1).value<KIMAP::Session *>();
+
+        KIMAP::CapabilitiesJob *job = new KIMAP::CapabilitiesJob(s);
+        job->start();
+        QTest::qWait(100);
+        QCOMPARE(lostSpy.count(), 1);
+        // FIXME extracting the pointer value form QVariant crashes
+        // QCOMPARE(lostSpy.at(0).at(0).value<KIMAP::Session *>(), s);
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+
+    void shouldNotifyOnDisconnect_data()
+    {
+        QTest::addColumn< QList<QByteArray> >("scenario");
+        QTest::addColumn<int>("termination");
+
+        QList<QByteArray> scenario;
+
+        scenario.clear();
+        scenario << FakeServer::greeting()
+                 << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""
+                 << "S: A000001 OK User Logged in"
+                 << "C: A000002 CAPABILITY"
+                 << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE"
+                 << "S: A000002 OK Completed"
+                 << "C: A000003 LOGOUT";
+
+        QTest::newRow("logout session") << scenario << (int)SessionPool::LogoutSession;
+
+        scenario.clear();
+        scenario << FakeServer::greeting()
+                 << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""
+                 << "S: A000001 OK User Logged in"
+                 << "C: A000002 CAPABILITY"
+                 << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE"
+                 << "S: A000002 OK Completed";
+
+        QTest::newRow("close session") << scenario << (int)SessionPool::CloseSession;
+    }
+
+    void shouldNotifyOnDisconnect()
+    {
+        QFETCH(QList<QByteArray>, scenario);
+        QFETCH(int, termination);
+
+        FakeServer server;
+        server.addScenario(scenario);
+
+        server.startAndWait();
+
+        ImapAccount *account = createDefaultAccount();
+        DummyPasswordRequester *requester = createDefaultRequester();
+
+        SessionPool pool(1);
+        pool.setPasswordRequester(requester);
+
+        QSignalSpy disconnectSpy(&pool, SIGNAL(disconnectDone()));
+
+        // Initial connect should trigger only a password request and a connect
+        QVERIFY(pool.connect(account));
+        QTest::qWait(100);
+
+        QCOMPARE(disconnectSpy.count(), 0);
+        pool.disconnect((SessionPool::SessionTermination) termination);
+        QTest::qWait(100);
+        QCOMPARE(disconnectSpy.count(), 1);
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+
+    void shouldCleanupOnClosingDuringLogin_data()
+    {
+        QTest::addColumn< QList<QByteArray> >("scenario");
+
+        {
+            QList<QByteArray> scenario;
+            scenario << FakeServer::greeting()
+                     << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\"";
+
+            QTest::newRow("during login") << scenario;
+        }
+        {
+            QList<QByteArray> scenario;
+            scenario << FakeServer::greeting()
+                     << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""
+                     << "S: A000001 OK User Logged in"
+                     << "C: A000002 CAPABILITY";
+
+            QTest::newRow("during capability") << scenario;
+        }
+        {
+            QList<QByteArray> scenario;
+            scenario << FakeServer::greeting()
+                     << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""
+                     << "S: A000001 OK User Logged in"
+                     << "C: A000002 CAPABILITY"
+                     << "S: * CAPABILITY IMAP4 IMAP4rev1 NAMESPACE UIDPLUS IDLE"
+                     << "S: A000002 OK Completed"
+                     << "C: A000003 NAMESPACE";
+            QTest::newRow("during namespace") << scenario;
+        }
+    }
+
+    void shouldCleanupOnClosingDuringLogin()
+    {
+        QFETCH(QList<QByteArray>, scenario);
+
+        FakeServer server;
+        server.addScenario(scenario);
+
+        server.startAndWait();
+
+        ImapAccount *account = createDefaultAccount();
+        DummyPasswordRequester *requester = createDefaultRequester();
+
+        SessionPool pool(1);
+        pool.setPasswordRequester(requester);
+
+        QSignalSpy connectSpy(&pool, SIGNAL(connectDone(int,QString)));
+        QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session*,int,QString)));
+        QSignalSpy lostSpy(&pool, SIGNAL(connectionLost(KIMAP::Session*)));
+
+        // Initial connect should trigger only a password request and a connect
+        QVERIFY(pool.connect(account));
+        QTest::qWait(100);
+        QCOMPARE(connectSpy.count(), 0);   // Login not done yet
+        QWeakPointer<KIMAP::Session> session = qFindChild<KIMAP::Session *>(&pool);
+        QVERIFY(session.data());
+        QCOMPARE(sessionSpy.count(), 0);
+
+        pool.disconnect(SessionPool::CloseSession);
+
+        QTest::qWait(100);
+        QCOMPARE(connectSpy.count(), 1);   // We're informed that connect failed
+        QCOMPARE(connectSpy.at(0).at(0).toInt(), int(SessionPool::CancelledError));
+        QCOMPARE(lostSpy.count(), 0);   // We're not supposed to know the session pointer, so no connectionLost emitted
+
+        // Make the session->deleteLater work, it can't happen in qWait (nested event loop)
+        QCoreApplication::sendPostedEvents(0, QEvent::DeferredDelete);
+
+        QVERIFY(session.isNull());
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+
+    void shouldHonorCancelRequest()
+    {
+        FakeServer server;
+        server.addScenario(QList<QByteArray>()
+                           << FakeServer::greeting()
+                           << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""
+                           << "S: A000001 OK User Logged in"
+                           << "C: A000002 CAPABILITY"
+                           << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE"
+                           << "S: A000002 OK Completed"
+                           << "C: A000003 CAPABILITY"
+                           << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE"
+                           << "X"
+                          );
+
+        server.startAndWait();
+
+        ImapAccount *account = createDefaultAccount();
+        DummyPasswordRequester *requester = createDefaultRequester();
+
+        SessionPool pool(1);
+        pool.setPasswordRequester(requester);
+
+        QSignalSpy connectSpy(&pool, SIGNAL(connectDone(int,QString)));
+        QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session*,int,QString)));
+        QSignalSpy lostSpy(&pool, SIGNAL(connectionLost(KIMAP::Session*)));
+
+        // Initial connect should trigger only a password request and a connect
+        QVERIFY(pool.connect(account));
+        QTest::qWait(100);
+        QCOMPARE(connectSpy.count(), 1);
+        QCOMPARE(sessionSpy.count(), 0);
+
+        qint64 requestId = pool.requestSession();
+
+        // Cancel the request
+        pool.cancelSessionRequest(requestId);
+
+        // The request should not be processed anymore
+        QTest::qWait(100);
+        QCOMPARE(sessionSpy.count(), 0);
+    }
+
+    void shouldBeDisconnectedOnAllSessionLost()
+    {
+        FakeServer server;
+        server.addScenario(QList<QByteArray>()
+                           << FakeServer::greeting()
+                           << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""
+                           << "S: A000001 OK User Logged in"
+                           << "C: A000002 CAPABILITY"
+                           << "S: * CAPABILITY IMAP4 IMAP4rev1 IDLE"
+                           << "S: A000002 OK Completed"
+                           << "C: A000003 CAPABILITY"
+                           << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE"
+                           << "X"
+                          );
+
+        server.addScenario(QList<QByteArray>()
+                           << FakeServer::greeting()
+                           << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""
+                           << "S: A000001 OK User Logged in"
+                           << "C: A000002 CAPABILITY"
+                           << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE"
+                           << "X"
+                          );
+
+        server.startAndWait();
+
+        ImapAccount *account = createDefaultAccount();
+        DummyPasswordRequester *requester = createDefaultRequester();
+
+        SessionPool pool(2);
+        pool.setPasswordRequester(requester);
+
+        QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session*,int,QString)));
+
+        // Initial connect should trigger only a password request and a connect
+        QVERIFY(pool.connect(account));
+        QTest::qWait(100);
+
+        // We should be connected now
+        QVERIFY(pool.isConnected());
+
+        // Ask for a session
+        pool.requestSession();
+        QTest::qWait(100);
+        QCOMPARE(sessionSpy.count(), 1);
+        QVERIFY(sessionSpy.at(0).at(1).value<KIMAP::Session *>() != 0);
+
+        // Still connected obviously
+        QVERIFY(pool.isConnected());
+
+        // Ask for a second session
+        pool.requestSession();
+        QTest::qWait(100);
+        QCOMPARE(sessionSpy.count(), 2);
+        QVERIFY(sessionSpy.at(1).at(1).value<KIMAP::Session *>() != 0);
+
+        // Still connected of course
+        QVERIFY(pool.isConnected());
+
+        KIMAP::Session *session1 = sessionSpy.at(0).at(1).value<KIMAP::Session *>();
+        KIMAP::Session *session2 = sessionSpy.at(1).at(1).value<KIMAP::Session *>();
+
+        // Prepare for session disconnects
+        QSignalSpy lostSpy(&pool, SIGNAL(connectionLost(KIMAP::Session*)));
+
+        // Make the first session drop
+        KIMAP::CapabilitiesJob *job = new KIMAP::CapabilitiesJob(session1);
+        job->start();
+        QTest::qWait(100);
+        QCOMPARE(lostSpy.count(), 1);
+        // FIXME extracting the pointer value form QVariant crashes
+        // QCOMPARE(lostSpy.at(0).at(0).value<KIMAP::Session *>(), session1);
+
+        // We're still connected (one session being alive)
+        QVERIFY(pool.isConnected());
+
+        // Make the second session drop
+        job = new KIMAP::CapabilitiesJob(session2);
+        job->start();
+        QTest::qWait(100);
+        QCOMPARE(lostSpy.count(), 2);
+        // FIXME extracting the pointer value form QVariant crashes
+        // QCOMPARE(lostSpy.at(1).at(0).value<KIMAP::Session *>(), session2);
+
+        // We're not connected anymore! All sessions dropped!
+        QVERIFY(!pool.isConnected());
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+
+    void shouldHandleDisconnectDuringPasswordRequest()
+    {
+        ImapAccount *account = createDefaultAccount();
+
+        // This requester will delay the second reply by a second
+        DummyPasswordRequester *requester = createDefaultRequester();
+        requester->setDelays(QList<int>() << 20 << 1000);
+        QSignalSpy requesterSpy(requester, SIGNAL(done(int,QString)));
+
+        FakeServer server;
+
+        server.addScenario(QList<QByteArray>()
+                           << FakeServer::greeting()
+                           << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""
+                           << "S: A000001 OK User Logged in"
+                           << "C: A000002 CAPABILITY"
+                           << "S: * CAPABILITY IMAP4 IMAP4rev1 IDLE"
+                           << "S: A000002 OK Completed"
+                           << "C: A000003 CAPABILITY"
+                           << "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE"
+                           << "X"
+                          );
+
+        server.startAndWait();
+
+        // The connect should work nicely
+        SessionPool pool(2);
+
+        QVERIFY(!pool.isConnected());
+
+        QSignalSpy poolSpy(&pool, SIGNAL(connectDone(int,QString)));
+        QSignalSpy sessionSpy(&pool, SIGNAL(sessionRequestDone(qint64,KIMAP::Session*,int,QString)));
+        QSignalSpy lostSpy(&pool, SIGNAL(connectionLost(KIMAP::Session*)));
+
+        pool.setPasswordRequester(requester);
+        QVERIFY(pool.connect(account));
+
+        QTest::qWait(100);
+        QCOMPARE(requesterSpy.count(), 1);
+
+        QCOMPARE(poolSpy.count(), 1);
+        QCOMPARE(poolSpy.at(0).at(0).toInt(), (int)SessionPool::NoError);
+        QVERIFY(pool.isConnected());
+
+        // Ask for the session we just created
+        pool.requestSession();
+        QTest::qWait(100);
+        QCOMPARE(sessionSpy.count(), 1);
+        QVERIFY(sessionSpy.at(0).at(1).value<KIMAP::Session *>() != 0);
+
+        KIMAP::Session *session = sessionSpy.at(0).at(1).value<KIMAP::Session *>();
+
+        // Ask for the second session, the password requested will never reply
+        // and we'll get a disconnect in parallel (by triggering the capability
+        // job on the first session
+        // Done this way to simulate a disconnect during a password request
+
+        // Ask for the extra session, and make sure the call is placed by waiting
+        // just a bit (but not too long so that the requester didn't reply yet,
+        // we set the reply timeout to 1000 earlier in this test)
+        pool.requestSession();
+        QTest::qWait(100);
+        QCOMPARE(requesterSpy.count(), 1);    // Requester didn't reply yet
+        QCOMPARE(sessionSpy.count(), 1);
+
+        // Make the first (and only) session drop while we wait for the requester
+        // to reply
+        KIMAP::CapabilitiesJob *job = new KIMAP::CapabilitiesJob(session);
+        job->start();
+        QTest::qWait(100);
+        QCOMPARE(lostSpy.count(), 1);
+        // FIXME extracting the pointer value form QVariant crashes
+        // QCOMPARE(lostSpy.at(0).at(0).value<KIMAP::Session *>(), session);
+
+        // The requester didn't reply yet
+        QCOMPARE(requesterSpy.count(), 1);
+        QCOMPARE(sessionSpy.count(), 1);
+
+        // Now wait the remaining time to get the session creation to fail
+        QTest::qWait(1000);
+        QCOMPARE(requesterSpy.count(), 2);
+        QCOMPARE(sessionSpy.count(), 2);
+        QCOMPARE(sessionSpy.at(1).at(2).toInt(), (int)SessionPool::LoginFailError);
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+
+    void shouldNotifyFailureToConnect()
+    {
+        // This tests what happens when we can't connect to the server, e.g. due to being offline.
+        // In this test we just use 0.0.0.0 as an invalid server IP, instead.
+        ImapAccount *account = createDefaultAccount();
+        account->setServer(QStringLiteral("0.0.0.0"));   // so that the connexion fails
+        DummyPasswordRequester *requester = createDefaultRequester();
+        QList<DummyPasswordRequester::RequestType> requests;
+        QList<DummyPasswordRequester::ResultType> results;
+        // I don't want to see "WrongPasswordRequest". A password popup is annoying when we're offline or the server is down.
+        requests << DummyPasswordRequester::StandardRequest;
+        results << DummyPasswordRequester::PasswordRetrieved;
+        requester->setScenario(requests, results);
+
+        QSignalSpy requesterSpy(requester, SIGNAL(done(int,QString)));
+        SessionPool pool(2);
+        QSignalSpy connectDoneSpy(&pool, SIGNAL(connectDone(int,QString)));
+        QSignalSpy lostSpy(&pool, SIGNAL(connectionLost(KIMAP::Session*)));
+        QVERIFY(!pool.isConnected());
+        pool.setPasswordRequester(requester);
+        pool.connect(account);
+        QVERIFY(!pool.isConnected());
+        QTRY_COMPARE(requesterSpy.count(), requests.count());
+        QTRY_COMPARE(connectDoneSpy.count(), 1);
+        QCOMPARE(connectDoneSpy.at(0).at(0).toInt(), (int)SessionPool::CouldNotConnectError);
+        QCOMPARE(lostSpy.count(), 0);   // don't want this, it makes the resource reconnect immediately (and fail, and reconnect, and so on...)
+    }
+
+};
+
+QTEST_GUILESS_MAIN(TestSessionPool)
+
+#include "testsessionpool.moc"
diff --git a/resources/imap/batchfetcher.cpp b/resources/imap/batchfetcher.cpp
new file mode 100644 (file)
index 0000000..8766dc4
--- /dev/null
@@ -0,0 +1,243 @@
+/*
+ * Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#include "batchfetcher.h"
+
+#include "imapresource_debug.h"
+#include <KIMAP/Session>
+#include "imapresource_debug.h"
+BatchFetcher::BatchFetcher(MessageHelper::Ptr messageHelper,
+                           const KIMAP::ImapSet &set,
+                           const KIMAP::FetchJob::FetchScope &scope,
+                           int batchSize,
+                           KIMAP::Session *session)
+    : KJob(session),
+      m_currentSet(set),
+      m_scope(scope),
+      m_session(session),
+      m_batchSize(batchSize),
+      m_uidBased(false),
+      m_fetchedItemsInCurrentBatch(0),
+      m_messageHelper(messageHelper),
+      m_fetchInProgress(false),
+      m_continuationRequested(false),
+      m_gmailEnabled(false),
+      m_searchInChunks(false)
+{
+}
+
+BatchFetcher::~BatchFetcher()
+{
+}
+
+void BatchFetcher::setUidBased(bool uidBased)
+{
+    m_uidBased = uidBased;
+}
+
+void BatchFetcher::setSearchUids(const KIMAP::ImapInterval &intervall)
+{
+    m_searchUidInterval = intervall;
+
+    //We look up the UIDs ourselves
+    m_currentSet = KIMAP::ImapSet();
+
+    //MS Exchange can't handle big results so we have to split the search into small chunks
+    m_searchInChunks = m_session->serverGreeting().contains("Microsoft Exchange");
+}
+
+void BatchFetcher::setGmailExtensionsEnabled(bool enable)
+{
+    m_gmailEnabled = enable;
+}
+
+static const int maxAmountOfUidToSearchInOneTime = 2000;
+
+void BatchFetcher::start()
+{
+    if (m_searchUidInterval.size()) {
+        //Search in chunks also Exchange can handle
+        const KIMAP::ImapInterval::Id firstUidToSearch = m_searchUidInterval.begin();
+        const KIMAP::ImapInterval::Id lastUidToSearch  = m_searchInChunks
+                ? qMin(firstUidToSearch + maxAmountOfUidToSearchInOneTime - 1, m_searchUidInterval.end())
+                : m_searchUidInterval.end();
+
+        //Prepare next chunk
+        const KIMAP::ImapInterval::Id intervalBegin = lastUidToSearch + 1;
+        //Or are we already done?
+        if (intervalBegin > m_searchUidInterval.end()) {
+            m_searchUidInterval = KIMAP::ImapInterval();
+        } else {
+            m_searchUidInterval.setBegin(intervalBegin);
+        }
+
+        //Resolve the uid to sequence numbers
+        KIMAP::SearchJob *search = new KIMAP::SearchJob(m_session);
+        search->setUidBased(true);
+        search->setTerm(KIMAP::Term(KIMAP::Term::Uid, KIMAP::ImapSet(firstUidToSearch, lastUidToSearch)));
+        connect(search, &KIMAP::SearchJob::result, this, &BatchFetcher::onUidSearchDone);
+        search->start();
+    } else {
+        fetchNextBatch();
+    }
+}
+
+void BatchFetcher::onUidSearchDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Search job failed: " << job->errorString();
+        setError(KJob::UserDefinedError);
+        emitResult();
+        return;
+    }
+
+    KIMAP::SearchJob *search = static_cast<KIMAP::SearchJob *>(job);
+    m_uidBased = search->isUidBased();
+    m_currentSet.add(search->results());
+
+    //More to search?
+    start();
+}
+
+void BatchFetcher::fetchNextBatch()
+{
+    if (m_fetchInProgress) {
+        m_continuationRequested = true;
+        return;
+    }
+    m_continuationRequested = false;
+    Q_ASSERT(m_batchSize > 0);
+    if (m_currentSet.isEmpty()) {
+        qCDebug(IMAPRESOURCE_LOG) << "fetch complete";
+        emitResult();
+        return;
+    }
+
+    KIMAP::FetchJob *fetch = new KIMAP::FetchJob(m_session);
+    if (m_scope.changedSince != 0) {
+        qCDebug(IMAPRESOURCE_LOG) << "Fetching all messages in one batch.";
+        fetch->setSequenceSet(m_currentSet);
+        m_currentSet = KIMAP::ImapSet();
+    } else {
+        KIMAP::ImapSet toFetch;
+        qint64 counter = 0;
+        KIMAP::ImapSet newSet;
+
+        //Take a chunk from the set
+        Q_FOREACH (const KIMAP::ImapInterval &interval, m_currentSet.intervals()) {
+            if (!interval.hasDefinedEnd()) {
+                //If we get an interval without a defined end we simply fetch everything
+                qCDebug(IMAPRESOURCE_LOG) << "Received interval without defined end, fetching everything in one batch";
+                toFetch.add(interval);
+                newSet = KIMAP::ImapSet();
+                break;
+            }
+            const qint64 wantedItems = m_batchSize - counter;
+            if (counter < m_batchSize) {
+                if (interval.size() <= wantedItems) {
+                    counter += interval.size();
+                    toFetch.add(interval);
+                } else {
+                    counter += wantedItems;
+                    toFetch.add(KIMAP::ImapInterval(interval.begin(), interval.begin() + wantedItems - 1));
+                    newSet.add(KIMAP::ImapInterval(interval.begin() + wantedItems, interval.end()));
+                }
+            } else {
+                newSet.add(interval);
+            }
+        }
+        qCDebug(IMAPRESOURCE_LOG) << "Fetching " << toFetch.intervals().size() << " intervals";
+        fetch->setSequenceSet(toFetch);
+        m_currentSet = newSet;
+    }
+
+    fetch->setUidBased(m_uidBased);
+    fetch->setScope(m_scope);
+    fetch->setGmailExtensionsEnabled(m_gmailEnabled);
+    connect(fetch, SIGNAL(headersReceived(QString,
+                                          QMap<qint64, qint64>,
+                                          QMap<qint64, qint64>,
+                                          QMap<qint64, KIMAP::MessageAttribute>,
+                                          QMap<qint64, KIMAP::MessageFlags>,
+                                          QMap<qint64, KIMAP::MessagePtr>)),
+            this, SLOT(onHeadersReceived(QString,
+                                         QMap<qint64, qint64>,
+                                         QMap<qint64, qint64>,
+                                         QMap<qint64, KIMAP::MessageAttribute>,
+                                         QMap<qint64, KIMAP::MessageFlags>,
+                                         QMap<qint64, KIMAP::MessagePtr>)));
+    connect(fetch, &KJob::result,
+            this, &BatchFetcher::onHeadersFetchDone);
+    m_fetchInProgress = true;
+    fetch->start();
+}
+
+void BatchFetcher::onHeadersReceived(const QString &mailBox,
+                                     const QMap<qint64, qint64> &uids,
+                                     const QMap<qint64, qint64> &sizes,
+                                     const QMap<qint64, KIMAP::MessageAttribute> &attrs,
+                                     const QMap<qint64, KIMAP::MessageFlags> &flags,
+                                     const QMap<qint64, KIMAP::MessagePtr> &messages)
+{
+    KIMAP::FetchJob *fetch = static_cast<KIMAP::FetchJob *>(sender());
+
+    Akonadi::Item::List addedItems;
+    foreach (qint64 number, uids.keys()) { //krazy:exclude=foreach
+        //qDebug( 5327 ) << "Flags: " << i.flags();
+        bool ok;
+        const Akonadi::Item item = m_messageHelper->createItemFromMessage(messages[number], uids[number], sizes[number], attrs.values(number), flags[number], fetch->scope(), ok);
+        if (ok) {
+            m_fetchedItemsInCurrentBatch++;
+            addedItems << item;
+        }
+    }
+//     qCDebug(IMAPRESOURCE_LOG) << addedItems.size();
+    if (!addedItems.isEmpty()) {
+        Q_EMIT itemsRetrieved(addedItems);
+    }
+}
+
+void BatchFetcher::onHeadersFetchDone(KJob *job)
+{
+    m_fetchInProgress = false;
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Fetch job failed " << job->errorString();
+        setError(KJob::UserDefinedError);
+        emitResult();
+        return;
+    }
+    if (m_currentSet.isEmpty()) {
+        emitResult();
+        return;
+    }
+    //Fetch more if we didn't deliver enough yet.
+    //This can happen because no message is in the fetched uid range, or if the translation failed
+    if (m_fetchedItemsInCurrentBatch < m_batchSize) {
+        fetchNextBatch();
+    } else {
+        m_fetchedItemsInCurrentBatch = 0;
+        //Also fetch more if we already got a continuation request during the fetch.
+        //This can happen if we deliver too many items during a previous batch (after using )
+        //Note that m_fetchedItemsInCurrentBatch will be off by the items that we delivered already.
+        if (m_continuationRequested) {
+            fetchNextBatch();
+        }
+    }
+}
+
diff --git a/resources/imap/batchfetcher.h b/resources/imap/batchfetcher.h
new file mode 100644 (file)
index 0000000..7d8706f
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifndef BATCHFETCHER_H
+#define BATCHFETCHER_H
+
+#include <KJob>
+#include <KIMAP/ImapSet>
+#include <KIMAP/FetchJob>
+#include <KIMAP/SearchJob>
+
+#include "messagehelper.h"
+
+/**
+ * A job that retrieves a set of messages in reverse-ordered batches.
+ * After each batch fetchNextBatch() needs to be called (for throttling the download speed)
+ */
+class BatchFetcher : public KJob
+{
+    Q_OBJECT
+public:
+    BatchFetcher(MessageHelper::Ptr messageHelper,
+                 const KIMAP::ImapSet &set,
+                 const KIMAP::FetchJob::FetchScope &scope,
+                 int batchSize,
+                 KIMAP::Session *session);
+    virtual ~BatchFetcher();
+    void start() Q_DECL_OVERRIDE;
+    void fetchNextBatch();
+    void setUidBased(bool);
+    void setSearchUids(const KIMAP::ImapInterval &);
+    void setGmailExtensionsEnabled(bool enable);
+
+Q_SIGNALS:
+    void itemsRetrieved(Akonadi::Item::List);
+
+private Q_SLOTS:
+    void onHeadersReceived(const QString &mailBox,
+                           const QMap<qint64, qint64> &uids,
+                           const QMap<qint64, qint64> &sizes,
+                           const QMap<qint64, KIMAP::MessageAttribute> &attrs,
+                           const QMap<qint64, KIMAP::MessageFlags> &flags,
+                           const QMap<qint64, KIMAP::MessagePtr> &messages);
+    void onHeadersFetchDone(KJob *job);
+    void onUidSearchDone(KJob *job);
+
+private:
+    //Batch fetching
+    KIMAP::ImapSet m_currentSet;
+    KIMAP::FetchJob::FetchScope m_scope;
+    KIMAP::Session *m_session;
+    int m_batchSize;
+    bool m_uidBased;
+    int m_fetchedItemsInCurrentBatch;
+    const MessageHelper::Ptr m_messageHelper;
+    bool m_fetchInProgress;
+    bool m_continuationRequested;
+    KIMAP::ImapInterval m_searchUidInterval;
+    bool m_gmailEnabled;
+    bool m_searchInChunks;
+};
+
+#endif // BATCHFETCHER_H
diff --git a/resources/imap/changecollectiontask.cpp b/resources/imap/changecollectiontask.cpp
new file mode 100644 (file)
index 0000000..d3d43d0
--- /dev/null
@@ -0,0 +1,311 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "changecollectiontask.h"
+
+#include <kimap/renamejob.h>
+#include <kimap/setacljob.h>
+#include <kimap/setmetadatajob.h>
+#include <kimap/session.h>
+#include <kimap/subscribejob.h>
+#include <kimap/unsubscribejob.h>
+
+#include "collectionannotationsattribute.h"
+#include "imapaclattribute.h"
+#include "imapquotaattribute.h"
+
+#include "imapresource_debug.h"
+#include <KLocalizedString>
+#include "imapresource_debug.h"
+
+ChangeCollectionTask::ChangeCollectionTask(const ResourceStateInterface::Ptr &resource, QObject *parent)
+    : ResourceTask(DeferIfNoSession, resource, parent), m_pendingJobs(0), m_syncEnabledState(true)
+{
+}
+
+ChangeCollectionTask::~ChangeCollectionTask()
+{
+}
+
+void ChangeCollectionTask::syncEnabledState(bool enable)
+{
+    m_syncEnabledState = enable;
+}
+
+void ChangeCollectionTask::doStart(KIMAP::Session *session)
+{
+    if (collection().remoteId().isEmpty()) {
+        emitError(i18n("Cannot modify IMAP folder '%1', it does not exist on the server.",
+                       collection().name()));
+        changeProcessed();
+        return;
+    }
+
+    m_collection = collection();
+    m_pendingJobs = 0;
+
+    if (parts().contains("AccessRights")) {
+        Akonadi::ImapAclAttribute *aclAttribute = m_collection.attribute<Akonadi::ImapAclAttribute>();
+
+        if (aclAttribute == Q_NULLPTR) {
+            emitWarning(i18n("ACLs for '%1' need to be retrieved from the IMAP server first. Skipping ACL change",
+                             collection().name()));
+        } else {
+            KIMAP::Acl::Rights imapRights = aclAttribute->rights().value(userName().toUtf8());
+            Akonadi::Collection::Rights newRights = collection().rights();
+
+            if (newRights & Akonadi::Collection::CanChangeItem) {
+                imapRights |= KIMAP::Acl::Write;
+            } else {
+                imapRights &= ~KIMAP::Acl::Write;
+            }
+
+            if (newRights & Akonadi::Collection::CanCreateItem) {
+                imapRights |= KIMAP::Acl::Insert;
+            } else {
+                imapRights &= ~KIMAP::Acl::Insert;
+            }
+
+            if (newRights & Akonadi::Collection::CanDeleteItem) {
+                imapRights |= KIMAP::Acl::DeleteMessage;
+            } else {
+                imapRights &= ~KIMAP::Acl::DeleteMessage;
+            }
+
+            if (newRights & (Akonadi::Collection::CanChangeCollection | Akonadi::Collection::CanCreateCollection)) {
+                imapRights |= KIMAP::Acl::CreateMailbox;
+                imapRights |= KIMAP::Acl::Create;
+            } else {
+                imapRights &= ~KIMAP::Acl::CreateMailbox;
+                imapRights &= ~KIMAP::Acl::Create;
+            }
+
+            if (newRights & Akonadi::Collection::CanDeleteCollection) {
+                imapRights |= KIMAP::Acl::DeleteMailbox;
+            } else {
+                imapRights &= ~KIMAP::Acl::DeleteMailbox;
+            }
+
+            if ((newRights & Akonadi::Collection::CanDeleteItem)
+                    && (newRights & Akonadi::Collection::CanDeleteCollection)) {
+                imapRights |= KIMAP::Acl::Delete;
+            } else {
+                imapRights &= ~KIMAP::Acl::Delete;
+            }
+
+            qCDebug(IMAPRESOURCE_LOG) << "imapRights:" << imapRights
+                                      << "newRights:" << newRights;
+
+            KIMAP::SetAclJob *job = new KIMAP::SetAclJob(session);
+            job->setMailBox(mailBoxForCollection(collection()));
+            job->setRights(KIMAP::SetAclJob::Change, imapRights);
+            job->setIdentifier(userName().toUtf8());
+
+            connect(job, &KIMAP::SetAclJob::result, this, &ChangeCollectionTask::onSetAclDone);
+
+            job->start();
+
+            m_pendingJobs++;
+        }
+    }
+
+    if (parts().contains("collectionannotations") && serverSupportsAnnotations()) {
+        Akonadi::Collection c = collection();
+        Akonadi::CollectionAnnotationsAttribute *annotationsAttribute =
+            c.attribute<Akonadi::CollectionAnnotationsAttribute>();
+
+        if (annotationsAttribute) {   // No annotations it seems... server is lieing to us?
+            QMap<QByteArray, QByteArray> annotations = annotationsAttribute->annotations();
+            qCDebug(IMAPRESOURCE_LOG) << "All annotations: " << annotations;
+
+            foreach (const QByteArray & entry, annotations.keys()) {  //krazy:exclude=foreach
+                KIMAP::SetMetaDataJob *job = new KIMAP::SetMetaDataJob(session);
+                if (serverCapabilities().contains(QStringLiteral("METADATA"))) {
+                    job->setServerCapability(KIMAP::MetaDataJobBase::Metadata);
+                } else {
+                    job->setServerCapability(KIMAP::MetaDataJobBase::Annotatemore);
+                }
+                job->setMailBox(mailBoxForCollection(collection()));
+
+                if (!entry.startsWith("/shared") && !entry.startsWith("/private")) {
+                    //Support for legacy annotations that don't include the prefix
+                    job->addMetaData(QByteArray("/shared") + entry, annotations[entry]);
+                } else {
+                    job->addMetaData(entry, annotations[entry]);
+                }
+
+                qCDebug(IMAPRESOURCE_LOG) << "Job got entry:" << entry << "value:" << annotations[entry];
+
+                connect(job, &KIMAP::SetMetaDataJob::result, this, &ChangeCollectionTask::onSetMetaDataDone);
+
+                job->start();
+
+                m_pendingJobs++;
+            }
+        }
+    }
+
+    if (parts().contains("imapacl")) {
+        Akonadi::Collection c = collection();
+        Akonadi::ImapAclAttribute *aclAttribute = c.attribute<Akonadi::ImapAclAttribute>();
+
+        if (aclAttribute) {
+            const QMap<QByteArray, KIMAP::Acl::Rights> rights = aclAttribute->rights();
+            const QMap<QByteArray, KIMAP::Acl::Rights> oldRights = aclAttribute->oldRights();
+            const QList<QByteArray> oldIds = oldRights.keys();
+            const QList<QByteArray> ids = rights.keys();
+
+            // remove all ACL entries that have been deleted
+            foreach (const QByteArray &oldId, oldIds) {
+                if (!ids.contains(oldId)) {
+                    KIMAP::SetAclJob *job = new KIMAP::SetAclJob(session);
+                    job->setMailBox(mailBoxForCollection(collection()));
+                    job->setIdentifier(oldId);
+                    job->setRights(KIMAP::SetAclJob::Remove, oldRights[oldId]);
+
+                    connect(job, &KIMAP::SetAclJob::result, this, &ChangeCollectionTask::onSetAclDone);
+
+                    job->start();
+
+                    m_pendingJobs++;
+                }
+            }
+
+            foreach (const QByteArray &id, ids) {
+                KIMAP::SetAclJob *job = new KIMAP::SetAclJob(session);
+                job->setMailBox(mailBoxForCollection(collection()));
+                job->setIdentifier(id);
+                job->setRights(KIMAP::SetAclJob::Change, rights[id]);
+
+                connect(job, &KIMAP::SetAclJob::result, this, &ChangeCollectionTask::onSetAclDone);
+
+                job->start();
+
+                m_pendingJobs++;
+            }
+        }
+    }
+
+    // Check if we need to rename the mailbox
+    // This one goes last on purpose, we don't want the previous jobs
+    // we triggered to act on the wrong mailbox name
+    if (parts().contains("NAME")) {
+        const QChar separator = separatorCharacter();
+        m_collection.setName(m_collection.name().replace(separator, QString()));
+        m_collection.setRemoteId(separator + m_collection.name());
+
+        const QString oldMailBox = mailBoxForCollection(collection());
+        const QString newMailBox = mailBoxForCollection(m_collection);
+
+        if (oldMailBox != newMailBox) {
+            KIMAP::RenameJob *renameJob = new KIMAP::RenameJob(session);
+            renameJob->setSourceMailBox(oldMailBox);
+            renameJob->setDestinationMailBox(newMailBox);
+            connect(renameJob, &KIMAP::RenameJob::result, this, &ChangeCollectionTask::onRenameDone);
+
+            renameJob->start();
+
+            m_pendingJobs++;
+        }
+    }
+
+    if (m_syncEnabledState && isSubscriptionEnabled() && parts().contains("ENABLED")) {
+        if (collection().enabled()) {
+            KIMAP::SubscribeJob *job = new KIMAP::SubscribeJob(session);
+            job->setMailBox(mailBoxForCollection(collection()));
+            connect(job, &KIMAP::SubscribeJob::result, this, &ChangeCollectionTask::onSubscribeDone);
+            job->start();
+        } else {
+            KIMAP::UnsubscribeJob *job = new KIMAP::UnsubscribeJob(session);
+            job->setMailBox(mailBoxForCollection(collection()));
+            connect(job, &KIMAP::UnsubscribeJob::result, this, &ChangeCollectionTask::onSubscribeDone);
+            job->start();
+        }
+        m_pendingJobs++;
+    }
+
+    // we scheduled no change on the server side, probably we got only
+    // unsupported part, so just declare the task done
+    if (m_pendingJobs == 0) {
+        changeCommitted(collection());
+    }
+}
+
+void ChangeCollectionTask::onRenameDone(KJob *job)
+{
+    if (job->error()) {
+        const QString prevRid = collection().remoteId();
+        Q_ASSERT(!prevRid.isEmpty());
+
+        emitWarning(i18n("Failed to rename the folder, restoring folder list."));
+
+        m_collection.setName(prevRid.mid(1));
+        m_collection.setRemoteId(prevRid);
+
+        endTaskIfNeeded();
+    } else {
+        KIMAP::RenameJob *renameJob = static_cast<KIMAP::RenameJob *>(job);
+        KIMAP::SubscribeJob *subscribeJob = new KIMAP::SubscribeJob(renameJob->session());
+        subscribeJob->setMailBox(renameJob->destinationMailBox());
+        connect(subscribeJob, &KIMAP::SubscribeJob::result, this, &ChangeCollectionTask::onSubscribeDone);
+        subscribeJob->start();
+    }
+}
+
+void ChangeCollectionTask::onSubscribeDone(KJob *job)
+{
+    if (job->error() && isSubscriptionEnabled()) {
+        emitWarning(i18n("Failed to subscribe to the renamed folder '%1' on the IMAP server. "
+                         "It will disappear on next sync. Use the subscription dialog to overcome that",
+                         m_collection.name()));
+    }
+
+    endTaskIfNeeded();
+}
+
+void ChangeCollectionTask::onSetAclDone(KJob *job)
+{
+    if (job->error()) {
+        emitWarning(i18n("Failed to write some ACLs for '%1' on the IMAP server. %2",
+                         collection().name(), job->errorText()));
+    }
+
+    endTaskIfNeeded();
+}
+
+void ChangeCollectionTask::onSetMetaDataDone(KJob *job)
+{
+    if (job->error()) {
+        emitWarning(i18n("Failed to write some annotations for '%1' on the IMAP server. %2",
+                         collection().name(), job->errorText()));
+    }
+
+    endTaskIfNeeded();
+}
+
+void ChangeCollectionTask::endTaskIfNeeded()
+{
+    if (--m_pendingJobs == 0) {
+        // the others have ended, we're done, the next one can go
+        changeCommitted(m_collection);
+    }
+}
+
diff --git a/resources/imap/changecollectiontask.h b/resources/imap/changecollectiontask.h
new file mode 100644 (file)
index 0000000..e0f69b1
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef CHANGECOLLECTIONTASK_H
+#define CHANGECOLLECTIONTASK_H
+
+#include "resourcetask.h"
+
+class ChangeCollectionTask : public ResourceTask
+{
+    Q_OBJECT
+
+public:
+    explicit ChangeCollectionTask(const ResourceStateInterface::Ptr &resource, QObject *parent = Q_NULLPTR);
+    virtual ~ChangeCollectionTask();
+
+    void syncEnabledState(bool);
+
+private Q_SLOTS:
+    void onRenameDone(KJob *job);
+    void onSubscribeDone(KJob *job);
+    void onSetAclDone(KJob *job);
+    void onSetMetaDataDone(KJob *job);
+
+protected:
+    void doStart(KIMAP::Session *session) Q_DECL_OVERRIDE;
+
+private:
+    void endTaskIfNeeded();
+
+    int m_pendingJobs;
+    Akonadi::Collection m_collection;
+    bool m_syncEnabledState;
+};
+
+#endif
diff --git a/resources/imap/changeitemsflagstask.cpp b/resources/imap/changeitemsflagstask.cpp
new file mode 100644 (file)
index 0000000..554eac3
--- /dev/null
@@ -0,0 +1,154 @@
+/*
+    Copyright (c) 2013 Daniel Vrátil <dvratil@redhat.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "changeitemsflagstask.h"
+
+#include <kimap/session.h>
+#include <kimap/selectjob.h>
+#include <kimap/storejob.h>
+#include "imapresource_debug.h"
+#include "imapresource_debug.h"
+
+ChangeItemsFlagsTask::ChangeItemsFlagsTask(const ResourceStateInterface::Ptr &resource, QObject *parent):
+    ResourceTask(ResourceTask::DeferIfNoSession, resource, parent),
+    m_processedItems(0)
+{
+
+}
+
+ChangeItemsFlagsTask::~ChangeItemsFlagsTask()
+{
+}
+
+void ChangeItemsFlagsTask::doStart(KIMAP::Session *session)
+{
+    const QString mailBox = mailBoxForCollection(items().at(0).parentCollection());
+    qCDebug(IMAPRESOURCE_LOG) << mailBox;
+
+    if (session->selectedMailBox() != mailBox) {
+        KIMAP::SelectJob *select = new KIMAP::SelectJob(session);
+        select->setMailBox(mailBox);
+
+        connect(select, &KJob::result,
+                this, &ChangeItemsFlagsTask::onSelectDone);
+
+        select->start();
+
+    } else {
+        if (!addedFlags().isEmpty()) {
+            triggerAppendFlagsJob(session);
+        } else if (!removedFlags().isEmpty()) {
+            triggerRemoveFlagsJob(session);
+        } else {
+            qCDebug(IMAPRESOURCE_LOG) << "nothing to do";
+            changeProcessed();
+        }
+    }
+}
+
+void ChangeItemsFlagsTask::onSelectDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Select failed: " << job->errorString();
+        cancelTask(job->errorString());
+    } else {
+        KIMAP::SelectJob *select = static_cast<KIMAP::SelectJob *>(job);
+        qCDebug(IMAPRESOURCE_LOG) << addedFlags();
+        if (!addedFlags().isEmpty()) {
+            triggerAppendFlagsJob(select->session());
+        } else if (!removedFlags().isEmpty()) {
+            triggerRemoveFlagsJob(select->session());
+        } else {
+            qCDebug(IMAPRESOURCE_LOG) << "nothing to do";
+            changeProcessed();
+        }
+    }
+}
+
+KIMAP::StoreJob *ChangeItemsFlagsTask::prepareJob(KIMAP::Session *session)
+{
+    KIMAP::ImapSet set;
+    const Akonadi::Item::List &allItems = items();
+    // Split the request to multiple smaller requests of 2000 UIDs each - various IMAP
+    // servers have various limits on maximum size of a request
+    // 2000 is a random number that sounds like a good compromise between performance
+    // and functionality (i.e. 2000 UIDs should be supported by any server out there)
+    for (int i = 0, count = qMin(2000, allItems.count() - m_processedItems); i < count; ++i) {
+        set.add(allItems[m_processedItems].remoteId().toLong());
+        ++m_processedItems;
+    }
+
+    KIMAP::StoreJob *store = new KIMAP::StoreJob(session);
+    store->setUidBased(true);
+    store->setSequenceSet(set);
+
+    return store;
+}
+
+void ChangeItemsFlagsTask::triggerAppendFlagsJob(KIMAP::Session *session)
+{
+    KIMAP::StoreJob *store = prepareJob(session);
+    store->setFlags(fromAkonadiToSupportedImapFlags(addedFlags().toList(), items().at(0).parentCollection()));
+    store->setMode(KIMAP::StoreJob::AppendFlags);
+    connect(store, &KIMAP::StoreJob::result, this, &ChangeItemsFlagsTask::onAppendFlagsDone);
+    store->start();
+}
+
+void ChangeItemsFlagsTask::triggerRemoveFlagsJob(KIMAP::Session *session)
+{
+    KIMAP::StoreJob *store = prepareJob(session);
+    store->setFlags(fromAkonadiToSupportedImapFlags(removedFlags().toList(), items().at(0).parentCollection()));
+    store->setMode(KIMAP::StoreJob::RemoveFlags);
+    connect(store, &KIMAP::StoreJob::result, this, &ChangeItemsFlagsTask::onRemoveFlagsDone);
+    store->start();
+}
+
+void ChangeItemsFlagsTask::onAppendFlagsDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Flag append failed: " << job->errorString();
+        cancelTask(job->errorString());
+    } else {
+        KIMAP::Session *session = qobject_cast<KIMAP::Job *>(job)->session();
+        if (m_processedItems < items().count()) {
+            triggerAppendFlagsJob(session);
+        } else if (removedFlags().isEmpty()) {
+            changeProcessed();
+        } else {
+            qCDebug(IMAPRESOURCE_LOG) << removedFlags();
+            m_processedItems = 0;
+            triggerRemoveFlagsJob(session);
+        }
+    }
+}
+
+void ChangeItemsFlagsTask::onRemoveFlagsDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Flag remove failed: " << job->errorString();
+        cancelTask(job->errorString());
+    } else {
+        if (m_processedItems < items().count()) {
+            triggerRemoveFlagsJob(qobject_cast<KIMAP::Job *>(job)->session());
+        } else {
+            changeProcessed();
+        }
+    }
+}
+
diff --git a/resources/imap/changeitemsflagstask.h b/resources/imap/changeitemsflagstask.h
new file mode 100644 (file)
index 0000000..ac7ca39
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+    Copyright (c) 2013 Daniel Vrátil <dvratil@redhat.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef CHANGEITEMSFLAGSTASK_H
+#define CHANGEITEMSFLAGSTASK_H
+
+#include "resourcetask.h"
+
+namespace KIMAP
+{
+class StoreJob;
+}
+
+class ChangeItemsFlagsTask : public ResourceTask
+{
+    Q_OBJECT
+
+public:
+    explicit ChangeItemsFlagsTask(const ResourceStateInterface::Ptr &resource, QObject *parent = Q_NULLPTR);
+    virtual ~ChangeItemsFlagsTask();
+
+protected Q_SLOTS:
+    void onSelectDone(KJob *job);
+    void onAppendFlagsDone(KJob *job);
+    void onRemoveFlagsDone(KJob *job);
+
+protected:
+    KIMAP::StoreJob *prepareJob(KIMAP::Session *session);
+
+    void doStart(KIMAP::Session *session) Q_DECL_OVERRIDE;
+
+    virtual void triggerAppendFlagsJob(KIMAP::Session *session);
+    virtual void triggerRemoveFlagsJob(KIMAP::Session *session);
+
+protected:
+    int m_processedItems;
+
+};
+
+#endif
diff --git a/resources/imap/changeitemtask.cpp b/resources/imap/changeitemtask.cpp
new file mode 100644 (file)
index 0000000..29f1b55
--- /dev/null
@@ -0,0 +1,296 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "changeitemtask.h"
+
+#include "imapresource_debug.h"
+
+#include <KLocalizedString>
+
+#include <kimap/appendjob.h>
+#include <kimap/searchjob.h>
+#include <kimap/selectjob.h>
+#include <kimap/session.h>
+#include <kimap/storejob.h>
+
+#include <kmime/kmime_message.h>
+
+#include "imapflags.h"
+#include "uidnextattribute.h"
+
+ChangeItemTask::ChangeItemTask(const ResourceStateInterface::Ptr &resource, QObject *parent)
+    : ResourceTask(DeferIfNoSession, resource, parent), m_session(Q_NULLPTR), m_oldUid(0), m_newUid(0)
+{
+
+}
+
+ChangeItemTask::~ChangeItemTask()
+{
+}
+
+void ChangeItemTask::doStart(KIMAP::Session *session)
+{
+    m_session = session;
+
+    const QString mailBox = mailBoxForCollection(item().parentCollection());
+    m_oldUid = item().remoteId().toLongLong();
+    qCDebug(IMAPRESOURCE_LOG) << mailBox << m_oldUid << parts();
+
+    if (parts().contains("PLD:RFC822")) {
+        if (!item().hasPayload<KMime::Message::Ptr>()) {
+            qCWarning(IMAPRESOURCE_LOG) << "Payload changed, no payload available.";
+            changeProcessed();
+            return;
+        }
+
+        // save message to the server.
+        KMime::Message::Ptr msg = item().payload<KMime::Message::Ptr>();
+        m_messageId = msg->messageID()->asUnicodeString().toUtf8();
+
+        KIMAP::AppendJob *job = new KIMAP::AppendJob(session);
+
+        job->setMailBox(mailBox);
+        job->setContent(msg->encodedContent(true));
+        const QList<QByteArray> flags = fromAkonadiToSupportedImapFlags(item().flags().toList(), item().parentCollection());
+        job->setFlags(flags);
+        qCDebug(IMAPRESOURCE_LOG) << "Appending new message: " << flags;
+
+        connect(job, &KIMAP::AppendJob::result, this, &ChangeItemTask::onAppendMessageDone);
+
+        job->start();
+
+    } else if (parts().contains("FLAGS")) {
+
+        if (session->selectedMailBox() != mailBox) {
+            KIMAP::SelectJob *select = new KIMAP::SelectJob(session);
+            select->setMailBox(mailBox);
+
+            connect(select, &KIMAP::SelectJob::result, this, &ChangeItemTask::onPreStoreSelectDone);
+
+            select->start();
+
+        } else {
+            triggerStoreJob();
+        }
+
+    } else {
+        qCDebug(IMAPRESOURCE_LOG) << "Nothing to do";
+        changeProcessed();
+    }
+}
+
+void ChangeItemTask::onPreStoreSelectDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Select failed: " << job->errorString();
+        cancelTask(job->errorString());
+    } else {
+        triggerStoreJob();
+    }
+}
+
+void ChangeItemTask::triggerStoreJob()
+{
+    QList<QByteArray> flags = fromAkonadiToSupportedImapFlags(item().flags().toList(), item().parentCollection());
+    qCDebug(IMAPRESOURCE_LOG) << flags;
+
+    KIMAP::StoreJob *store = new KIMAP::StoreJob(m_session);
+
+    store->setUidBased(true);
+    store->setSequenceSet(KIMAP::ImapSet(m_oldUid));
+    store->setFlags(flags);
+    store->setMode(KIMAP::StoreJob::SetFlags);
+
+    connect(store, &KIMAP::StoreJob::result, this, &ChangeItemTask::onStoreFlagsDone);
+
+    store->start();
+}
+
+void ChangeItemTask::onStoreFlagsDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Flag store failed: " << job->errorString();
+        cancelTask(job->errorString());
+    } else {
+        changeProcessed();
+    }
+}
+
+void ChangeItemTask::onAppendMessageDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Append failed: " << job->errorString();
+        cancelTask(job->errorString());
+        return;
+    }
+
+    KIMAP::AppendJob *append = qobject_cast<KIMAP::AppendJob *>(job);
+
+    m_newUid = append->uid();
+
+    // OK it's a content change, so we've to mark the old version as deleted
+    // remember, you can't modify messages in IMAP mailboxes so that's really
+    // add+remove all the time.
+
+    // APPEND does not require a SELECT, so we could be anywhere right now
+    if (m_session->selectedMailBox() != append->mailBox()) {
+        KIMAP::SelectJob *select = new KIMAP::SelectJob(m_session);
+        select->setMailBox(append->mailBox());
+
+        connect(select, &KIMAP::SelectJob::result, this, &ChangeItemTask::onPreDeleteSelectDone);
+
+        select->start();
+
+    } else {
+        if (m_newUid > 0) {
+            triggerDeleteJob();
+        } else {
+            triggerSearchJob();
+        }
+    }
+}
+
+void ChangeItemTask::onPreDeleteSelectDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "PreDelete select failed: " << job->errorString();
+        if (m_newUid > 0) {
+            recordNewUid();
+        } else {
+            cancelTask(job->errorString());
+        }
+    } else {
+        if (m_newUid > 0) {
+            triggerDeleteJob();
+        } else {
+            triggerSearchJob();
+        }
+    }
+}
+
+void ChangeItemTask::triggerSearchJob()
+{
+    KIMAP::SearchJob *search = new KIMAP::SearchJob(m_session);
+
+    search->setUidBased(true);
+    search->setSearchLogic(KIMAP::SearchJob::And);
+
+    if (!m_messageId.isEmpty()) {
+        QByteArray header = "Message-ID ";
+        header += m_messageId;
+
+        search->addSearchCriteria(KIMAP::SearchJob::Header, header);
+    } else {
+        search->addSearchCriteria(KIMAP::SearchJob::New);
+
+        UidNextAttribute *uidNext = item().parentCollection().attribute<UidNextAttribute>();
+        if (!uidNext) {
+            qCWarning(IMAPRESOURCE_LOG) << "Failed to determine new uid.";
+            cancelTask(i18n("Could not determine the UID for the newly created message on the server"));
+            search->deleteLater();
+            return;
+        }
+        KIMAP::ImapInterval interval(uidNext->uidNext());
+
+        search->addSearchCriteria(KIMAP::SearchJob::Uid, interval.toImapSequence());
+    }
+
+    connect(search, &KIMAP::SearchJob::result, this, &ChangeItemTask::onSearchDone);
+
+    search->start();
+}
+
+void ChangeItemTask::onSearchDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Search failed: " << job->errorString();
+        cancelTask(job->errorString());
+        return;
+    }
+
+    KIMAP::SearchJob *search = static_cast<KIMAP::SearchJob *>(job);
+
+    if (search->results().count() != 1) {
+        qCWarning(IMAPRESOURCE_LOG) << "Failed to determine new uid.";
+        cancelTask(i18n("Could not determine the UID for the newly created message on the server"));
+        return;
+    }
+
+    m_newUid = search->results().at(0);
+    triggerDeleteJob();
+}
+
+void ChangeItemTask::triggerDeleteJob()
+{
+    KIMAP::StoreJob *store = new KIMAP::StoreJob(m_session);
+
+    store->setUidBased(true);
+    store->setSequenceSet(KIMAP::ImapSet(m_oldUid));
+    store->setFlags(QList<QByteArray>() << ImapFlags::Deleted);
+    store->setMode(KIMAP::StoreJob::AppendFlags);
+
+    connect(store, &KIMAP::StoreJob::result, this, &ChangeItemTask::onDeleteDone);
+
+    store->start();
+}
+
+void ChangeItemTask::onDeleteDone(KJob */*job*/)
+{
+    recordNewUid();
+}
+
+void ChangeItemTask::recordNewUid()
+{
+    Q_ASSERT(m_newUid > 0);
+
+    Akonadi::Item i = item();
+    Akonadi::Collection c = i.parentCollection();
+
+    // Get the current uid next value and store it
+    UidNextAttribute *uidAttr = Q_NULLPTR;
+    int oldNextUid = 0;
+    if (c.hasAttribute("uidnext")) {
+        uidAttr = static_cast<UidNextAttribute *>(c.attribute("uidnext"));
+        oldNextUid = uidAttr->uidNext();
+    }
+
+    // If the uid we just got back is the expected next one of the box
+    // then update the property to the probable next uid to keep the cache in sync.
+    // If not something happened in our back, so we don't update and a refetch will
+    // happen at some point.
+    if (m_newUid == oldNextUid) {
+        if (uidAttr == Q_NULLPTR) {
+            uidAttr = new UidNextAttribute(m_newUid + 1);
+            c.addAttribute(uidAttr);
+        } else {
+            uidAttr->setUidNext(m_newUid + 1);
+        }
+
+        applyCollectionChanges(c);
+    }
+
+    const QString remoteId =  QString::number(m_newUid);
+    qCDebug(IMAPRESOURCE_LOG) << "Setting remote ID to " << remoteId << " for item with local id " << i.id();
+    i.setRemoteId(remoteId);
+
+    changeCommitted(i);
+}
+
diff --git a/resources/imap/changeitemtask.h b/resources/imap/changeitemtask.h
new file mode 100644 (file)
index 0000000..4520e00
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef CHANGEITEMTASK_H
+#define CHANGEITEMTASK_H
+
+#include "resourcetask.h"
+
+class ChangeItemTask : public ResourceTask
+{
+    Q_OBJECT
+
+public:
+    explicit ChangeItemTask(const ResourceStateInterface::Ptr &resource, QObject *parent = Q_NULLPTR);
+    virtual ~ChangeItemTask();
+
+private Q_SLOTS:
+    void onAppendMessageDone(KJob *job);
+
+    void onPreStoreSelectDone(KJob *job);
+    void onStoreFlagsDone(KJob *job);
+
+    void onPreDeleteSelectDone(KJob *job);
+    void onSearchDone(KJob *job);
+    void onDeleteDone(KJob *job);
+
+protected:
+    void doStart(KIMAP::Session *session) Q_DECL_OVERRIDE;
+
+private:
+    void triggerStoreJob();
+    void triggerSearchJob();
+    void triggerDeleteJob();
+
+    void recordNewUid();
+
+    KIMAP::Session *m_session;
+    QByteArray m_messageId;
+    qint64 m_oldUid;
+    qint64 m_newUid;
+};
+
+#endif
diff --git a/resources/imap/collectionmetadatahelper.cpp b/resources/imap/collectionmetadatahelper.cpp
new file mode 100644 (file)
index 0000000..39335bb
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "collectionmetadatahelper.h"
+#include <imapaclattribute.h>
+
+Akonadi::Collection::Rights CollectionMetadataHelper::convertRights(const KIMAP::Acl::Rights imapRights, KIMAP::Acl::Rights parentRights)
+{
+    Akonadi::Collection::Rights newRights = Akonadi::Collection::ReadOnly;
+
+    // For renaming, the parent folder needs to have the CreateMailbox or Create permission.
+    // We map renaming to CanChangeCollection here, which is not entirely correct, but we have no
+    // CanRenameCollection flag.
+    if (parentRights & KIMAP::Acl::CreateMailbox ||
+            parentRights & KIMAP::Acl::Create) {
+        newRights |= Akonadi::Collection::CanChangeCollection;
+    }
+
+    if (imapRights & KIMAP::Acl::Write) {
+        newRights |= Akonadi::Collection::CanChangeItem;
+    }
+
+    if (imapRights & KIMAP::Acl::Insert) {
+        newRights |= Akonadi::Collection::CanCreateItem;
+    }
+
+    if (imapRights & (KIMAP::Acl::DeleteMessage | KIMAP::Acl::Delete)) {
+        newRights |= Akonadi::Collection::CanDeleteItem;
+    }
+
+    if (imapRights & (KIMAP::Acl::CreateMailbox | KIMAP::Acl::Create)) {
+        newRights |= Akonadi::Collection::CanCreateCollection;
+    }
+
+    if (imapRights & (KIMAP::Acl::DeleteMailbox | KIMAP::Acl::Delete)) {
+        newRights |= Akonadi::Collection::CanDeleteCollection;
+    }
+    return newRights;
+}
+
+bool CollectionMetadataHelper::applyRights(Akonadi::Collection &collection, const KIMAP::Acl::Rights imapRights, KIMAP::Acl::Rights parentRights)
+{
+    Akonadi::Collection::Rights newRights = convertRights(imapRights, parentRights);
+
+    if (collection.hasAttribute("noinferiors")) {
+        newRights &= ~Akonadi::Collection::CanCreateCollection;
+    }
+
+    if (collection.parentCollection().hasAttribute("noselect")) {
+        newRights &= ~Akonadi::Collection::CanChangeCollection;
+    }
+
+    //This can result in false positives for new collections with defaults access rights.
+    //The caller needs to handles those.
+    bool accessRevoked = false;
+    if ((collection.rights() & Akonadi::Collection::CanCreateItem) &&
+            !(newRights & Akonadi::Collection::CanCreateItem)) {
+        // write access revoked
+        accessRevoked = true;
+    }
+
+    // kDebug(5327) << collection.remoteId()
+    //             << "imapRights:" << imapRights
+    //             << "newRights:" << newRights
+    //             << "oldRights:" << collection.rights();
+
+    if (newRights != collection.rights()) {
+        collection.setRights(newRights);
+    }
+    return accessRevoked;
+}
+
diff --git a/resources/imap/collectionmetadatahelper.h b/resources/imap/collectionmetadatahelper.h
new file mode 100644 (file)
index 0000000..e34d9c2
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+#ifndef COLLECTIONMETADATAHELPER_H
+#define COLLECTIONMETADATAHELPER_H
+
+#include <AkonadiCore/Collection>
+#include <kimap/acl.h>
+
+class CollectionMetadataHelper
+{
+public:
+    static Akonadi::Collection::Rights convertRights(const KIMAP::Acl::Rights imapRights, KIMAP::Acl::Rights parentRights);
+    static bool applyRights(Akonadi::Collection &collection, const KIMAP::Acl::Rights imapRights, KIMAP::Acl::Rights parentRights);
+};
+
+#endif
diff --git a/resources/imap/expungecollectiontask.cpp b/resources/imap/expungecollectiontask.cpp
new file mode 100644 (file)
index 0000000..2a01fee
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "expungecollectiontask.h"
+
+#include "imapresource_debug.h"
+#include "imapresource_debug.h"
+
+#include <KLocalizedString>
+
+#include <kimap/expungejob.h>
+#include <kimap/selectjob.h>
+#include <kimap/session.h>
+
+#include "noselectattribute.h"
+
+ExpungeCollectionTask::ExpungeCollectionTask(ResourceStateInterface::Ptr resource, QObject *parent)
+    : ResourceTask(CancelIfNoSession, resource, parent)
+{
+
+}
+
+ExpungeCollectionTask::~ExpungeCollectionTask()
+{
+}
+
+void ExpungeCollectionTask::doStart(KIMAP::Session *session)
+{
+    // Prevent expunging items from noselect folders.
+    if (collection().hasAttribute("noselect")) {
+        NoSelectAttribute *noselect = static_cast<NoSelectAttribute *>(collection().attribute("noselect"));
+        if (noselect->noSelect()) {
+            qCDebug(IMAPRESOURCE_LOG) << "No Select folder";
+            taskDone();
+            return;
+        }
+    }
+
+    const QString mailBox = mailBoxForCollection(collection());
+
+    if (session->selectedMailBox() != mailBox) {
+        KIMAP::SelectJob *select = new KIMAP::SelectJob(session);
+        select->setMailBox(mailBox);
+
+        connect(select, &KIMAP::SelectJob::result, this, &ExpungeCollectionTask::onSelectDone);
+
+        select->start();
+
+    } else {
+        triggerExpungeJob(session);
+    }
+}
+
+void ExpungeCollectionTask::onSelectDone(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(job->errorString());
+    } else {
+        KIMAP::SelectJob *select = static_cast<KIMAP::SelectJob *>(job);
+        triggerExpungeJob(select->session());
+    }
+}
+
+void ExpungeCollectionTask::triggerExpungeJob(KIMAP::Session *session)
+{
+    KIMAP::ExpungeJob *expunge = new KIMAP::ExpungeJob(session);
+
+    connect(expunge, &KIMAP::ExpungeJob::result, this, &ExpungeCollectionTask::onExpungeDone);
+
+    expunge->start();
+}
+
+void ExpungeCollectionTask::onExpungeDone(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(job->errorString());
+    } else {
+        taskDone();
+    }
+}
+
diff --git a/resources/imap/expungecollectiontask.h b/resources/imap/expungecollectiontask.h
new file mode 100644 (file)
index 0000000..9aebfe4
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef EXPUNGECOLLECTIONTASK_H
+#define EXPUNGECOLLECTIONTASK_H
+
+#include "resourcetask.h"
+
+class ExpungeCollectionTask : public ResourceTask
+{
+    Q_OBJECT
+
+public:
+    explicit ExpungeCollectionTask(ResourceStateInterface::Ptr resource, QObject *parent = Q_NULLPTR);
+    virtual ~ExpungeCollectionTask();
+
+private Q_SLOTS:
+    void onSelectDone(KJob *job);
+    void onExpungeDone(KJob *job);
+
+protected:
+    void doStart(KIMAP::Session *session) Q_DECL_OVERRIDE;
+
+private:
+    void triggerExpungeJob(KIMAP::Session *session);
+};
+
+#endif
diff --git a/resources/imap/highestmodseqattribute.cpp b/resources/imap/highestmodseqattribute.cpp
new file mode 100644 (file)
index 0000000..35cc1a1
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2013  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+#include "highestmodseqattribute.h"
+
+#include <QtCore/QByteArray>
+
+HighestModSeqAttribute::HighestModSeqAttribute(qint64 highestModSequence):
+    Akonadi::Attribute(),
+    m_highestModSeq(highestModSequence)
+{
+}
+
+void HighestModSeqAttribute::setHighestModSeq(qint64 highestModSequence)
+{
+    m_highestModSeq = highestModSequence;
+}
+
+qint64 HighestModSeqAttribute::highestModSequence() const
+{
+    return m_highestModSeq;
+}
+
+Akonadi::Attribute *HighestModSeqAttribute::clone() const
+{
+    return new HighestModSeqAttribute(m_highestModSeq);
+}
+
+QByteArray HighestModSeqAttribute::type() const
+{
+    static const QByteArray sType("highestmodseq");
+    return sType;
+}
+
+void HighestModSeqAttribute::deserialize(const QByteArray &data)
+{
+    m_highestModSeq = data.toLongLong();
+}
+
+QByteArray HighestModSeqAttribute::serialized() const
+{
+    return QByteArray::number(m_highestModSeq);
+}
diff --git a/resources/imap/highestmodseqattribute.h b/resources/imap/highestmodseqattribute.h
new file mode 100644 (file)
index 0000000..f0ab649
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2013  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+#ifndef HIGHESTMODSEQATTRIBUTE_H
+#define HIGHESTMODSEQATTRIBUTE_H
+
+#include <Attribute>
+
+class HighestModSeqAttribute : public Akonadi::Attribute
+{
+public:
+    explicit HighestModSeqAttribute(qint64 highestModSequence = -1);
+    void setHighestModSeq(qint64 highestModSequence);
+    qint64 highestModSequence() const;
+
+    void deserialize(const QByteArray &data) Q_DECL_OVERRIDE;
+    QByteArray serialized() const Q_DECL_OVERRIDE;
+    Akonadi::Attribute *clone() const Q_DECL_OVERRIDE;
+    QByteArray type() const Q_DECL_OVERRIDE;
+
+private:
+    qint64 m_highestModSeq;
+};
+
+#endif // HIGHESTMODSEQATTRIBUTE_H
diff --git a/resources/imap/imapaccount.cpp b/resources/imap/imapaccount.cpp
new file mode 100644 (file)
index 0000000..16d827c
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+    Copyright (c) 2007 Till Adam <adam@kde.org>
+    Copyright (C) 2008 Omat Holding B.V. <info@omat.nl>
+    Copyright (C) 2009 Kevin Ottens <ervin@kde.org>
+
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "imapaccount.h"
+
+ImapAccount::ImapAccount()
+    : m_port(0),
+      m_timeout(30),
+      m_encryption(KIMAP::LoginJob::Unencrypted),
+      m_authentication(KIMAP::LoginJob::ClearText),
+      m_subscriptionEnabled(false)
+{
+}
+
+ImapAccount::~ImapAccount()
+{
+}
+
+void ImapAccount::setServer(const QString &server)
+{
+    m_server = server;
+}
+
+QString ImapAccount::server() const
+{
+    return m_server;
+}
+
+void ImapAccount::setPort(quint16 port)
+{
+    m_port = port;
+}
+
+quint16 ImapAccount::port() const
+{
+    return m_port;
+}
+
+void ImapAccount::setUserName(const QString &userName)
+{
+    m_userName = userName;
+}
+
+QString ImapAccount::userName() const
+{
+    return m_userName;
+}
+
+void ImapAccount::setEncryptionMode(KIMAP::LoginJob::EncryptionMode mode)
+{
+    m_encryption = mode;
+}
+
+KIMAP::LoginJob::EncryptionMode ImapAccount::encryptionMode() const
+{
+    return m_encryption;
+}
+
+void ImapAccount::setAuthenticationMode(KIMAP::LoginJob::AuthenticationMode mode)
+{
+    m_authentication = mode;
+}
+
+KIMAP::LoginJob::AuthenticationMode ImapAccount::authenticationMode() const
+{
+    return m_authentication;
+}
+
+void ImapAccount::setSubscriptionEnabled(bool enabled)
+{
+    m_subscriptionEnabled = enabled;
+}
+
+bool ImapAccount::isSubscriptionEnabled() const
+{
+    return m_subscriptionEnabled;
+}
+
+void ImapAccount::setTimeout(int timeout)
+{
+    m_timeout = timeout;
+}
+
+int ImapAccount::timeout() const
+{
+    return m_timeout;
+}
diff --git a/resources/imap/imapaccount.h b/resources/imap/imapaccount.h
new file mode 100644 (file)
index 0000000..fc8bdbf
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+    Copyright (c) 2007 Till Adam <adam@kde.org>
+    Copyright (C) 2008 Omat Holding B.V. <info@omat.nl>
+    Copyright (C) 2009 Kevin Ottens <ervin@kde.org>
+
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef IMAPACCOUNT_H
+#define IMAPACCOUNT_H
+
+#include <kimap/loginjob.h>
+
+class ImapAccount
+{
+public:
+    explicit ImapAccount();
+    ~ImapAccount();
+
+    void setServer(const QString &server);
+    QString server() const;
+
+    void setPort(quint16 port);
+    quint16 port() const;
+
+    void setUserName(const QString &userName);
+    QString userName() const;
+
+    void setEncryptionMode(KIMAP::LoginJob::EncryptionMode mode);
+    KIMAP::LoginJob::EncryptionMode encryptionMode() const;
+
+    void setAuthenticationMode(KIMAP::LoginJob::AuthenticationMode mode);
+    KIMAP::LoginJob::AuthenticationMode authenticationMode() const;
+
+    void setSubscriptionEnabled(bool enabled);
+    bool isSubscriptionEnabled() const;
+
+    void setTimeout(int timeout);
+    int timeout() const;
+
+private:
+    QString m_name;
+    QString m_server;
+    quint16 m_port;
+    QString m_userName;
+    int m_timeout;
+    KIMAP::LoginJob::EncryptionMode m_encryption;
+    KIMAP::LoginJob::AuthenticationMode m_authentication;
+    bool m_subscriptionEnabled;
+};
+
+#endif
diff --git a/resources/imap/imapflags.cpp b/resources/imap/imapflags.cpp
new file mode 100644 (file)
index 0000000..1041fee
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2010 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
+ * Copyright (c) 2010 Leo Franchi <lfranchi@kde.org>
+ *
+ * This library is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB.  If not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+#include "imapflags.h"
+
+const char *ImapFlags::Seen = "\\Seen";
+const char *ImapFlags::Deleted = "\\Deleted";
+const char *ImapFlags::Answered = "\\Answered";
+const char *ImapFlags::Flagged = "\\Flagged";
diff --git a/resources/imap/imapflags.h b/resources/imap/imapflags.h
new file mode 100644 (file)
index 0000000..d155782
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2010 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
+ * Copyright (c) 2010 Leo Franchi <lfranchi@kde.org>
+ *
+ * This library is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public License
+ * along with this library; see the file COPYING.LIB.  If not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+#ifndef IMAP_RESOURCE_IMAPFLAGS_H
+#define IMAP_RESOURCE_IMAPFLAGS_H
+
+/**
+ * Contains constants for IMAP flags from KIMAP.
+ */
+namespace ImapFlags
+{
+/**
+  * The flag for a message being seen (i.e. opened by user).
+  */
+extern const char *Seen;
+
+/**
+  * The flag for a message being deleted by the user.
+  */
+extern const char *Deleted;
+
+/**
+  * The flag for a message being replied to by the user.
+  */
+extern const char *Answered;
+
+/**
+  * The flag for a message being marked as flagged.
+  */
+extern const char *Flagged;
+
+}
+
+#endif
diff --git a/resources/imap/imapidlemanager.cpp b/resources/imap/imapidlemanager.cpp
new file mode 100644 (file)
index 0000000..030f34f
--- /dev/null
@@ -0,0 +1,175 @@
+/*
+    Copyright (c) 2007 Till Adam <adam@kde.org>
+    Copyright (C) 2008 Omat Holding B.V. <info@omat.nl>
+    Copyright (C) 2009 Kevin Ottens <ervin@kde.org>
+
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "imapidlemanager.h"
+
+#include "imapresource_debug.h"
+#include "imapresource_debug.h"
+
+#include <kimap/idlejob.h>
+#include <kimap/selectjob.h>
+#include <kimap/session.h>
+
+#include <QtCore/QTimer>
+
+#include "imapresource.h"
+#include "sessionpool.h"
+
+ImapIdleManager::ImapIdleManager(ResourceStateInterface::Ptr state,
+                                 SessionPool *pool, ImapResourceBase *parent)
+    : QObject(parent), m_sessionRequestId(0), m_pool(pool), m_session(Q_NULLPTR),
+      m_idle(Q_NULLPTR), m_resource(parent), m_state(state),
+      m_lastMessageCount(-1), m_lastRecentCount(-1)
+{
+    connect(pool, &SessionPool::sessionRequestDone, this, &ImapIdleManager::onSessionRequestDone);
+    m_sessionRequestId = m_pool->requestSession();
+}
+
+ImapIdleManager::~ImapIdleManager()
+{
+    stop();
+    if (m_pool) {
+        if (m_sessionRequestId) {
+            m_pool->cancelSessionRequest(m_sessionRequestId);
+        }
+        if (m_session) {
+            m_pool->releaseSession(m_session);
+        }
+    }
+}
+
+void ImapIdleManager::stop()
+{
+    if (m_idle) {
+        m_idle->stop();
+        disconnect(m_idle, Q_NULLPTR, this, Q_NULLPTR);
+        m_idle = Q_NULLPTR;
+    }
+    if (m_pool) {
+        disconnect(m_pool, Q_NULLPTR, this, Q_NULLPTR);
+    }
+}
+
+KIMAP::Session *ImapIdleManager::session() const
+{
+    return m_session;
+}
+
+void ImapIdleManager::reconnect()
+{
+    qCDebug(IMAPRESOURCE_LOG) << "attempting to reconnect IDLE session";
+    if (m_session == Q_NULLPTR && m_pool->isConnected() && m_sessionRequestId == 0) {
+        m_sessionRequestId = m_pool->requestSession();
+    }
+}
+
+void ImapIdleManager::onSessionRequestDone(qint64 requestId, KIMAP::Session *session,
+        int errorCode, const QString &/*errorString*/)
+{
+    if (requestId != m_sessionRequestId || session == Q_NULLPTR || errorCode != SessionPool::NoError) {
+        return;
+    }
+
+    m_session = session;
+    m_sessionRequestId = 0;
+
+    connect(m_pool, &SessionPool::connectionLost, this, &ImapIdleManager::onConnectionLost);
+    connect(m_pool, &SessionPool::disconnectDone, this, &ImapIdleManager::onPoolDisconnect);
+
+    KIMAP::SelectJob *select = new KIMAP::SelectJob(m_session);
+    select->setMailBox(m_state->mailBoxForCollection(m_state->collection()));
+    connect(select, &KIMAP::SelectJob::result, this, &ImapIdleManager::onSelectDone);
+    select->start();
+
+    m_idle = new KIMAP::IdleJob(m_session);
+    connect(m_idle.data(), &KIMAP::IdleJob::mailBoxStats, this, &ImapIdleManager::onStatsReceived);
+    connect(m_idle.data(), &KIMAP::IdleJob::mailBoxMessageFlagsChanged, this, &ImapIdleManager::onFlagsChanged);
+    connect(m_idle.data(), &KIMAP::IdleJob::result, this, &ImapIdleManager::onIdleStopped);
+    m_idle->start();
+}
+
+void ImapIdleManager::onConnectionLost(KIMAP::Session *session)
+{
+    if (session == m_session) {
+        // Our session becomes invalid, so get ride of
+        // the pointer, we don't need to release it once the
+        // task is done
+        m_session = Q_NULLPTR;
+        QMetaObject::invokeMethod(this, "reconnect", Qt::QueuedConnection);
+    }
+}
+
+void ImapIdleManager::onPoolDisconnect()
+{
+    // All the sessions in the pool we used changed,
+    // so get ride of the pointer, we don't need to
+    // release our session anymore
+    m_pool = Q_NULLPTR;
+}
+
+void ImapIdleManager::onSelectDone(KJob *job)
+{
+    KIMAP::SelectJob *select = static_cast<KIMAP::SelectJob *>(job);
+
+    m_lastMessageCount = select->messageCount();
+    m_lastRecentCount = select->recentCount();
+}
+
+void ImapIdleManager::onIdleStopped()
+{
+    qCDebug(IMAPRESOURCE_LOG) << "IDLE dropped maybe we should reconnect?";
+    m_idle = Q_NULLPTR;
+    if (m_session) {
+        qCDebug(IMAPRESOURCE_LOG) << "Restarting the IDLE session!";
+        m_idle = new KIMAP::IdleJob(m_session);
+        connect(m_idle.data(), &KIMAP::IdleJob::mailBoxStats, this, &ImapIdleManager::onStatsReceived);
+        connect(m_idle.data(), &KIMAP::IdleJob::result, this, &ImapIdleManager::onIdleStopped);
+        m_idle->start();
+    }
+}
+
+void ImapIdleManager::onStatsReceived(KIMAP::IdleJob *job, const QString &mailBox,
+                                      int messageCount, int recentCount)
+{
+    qCDebug(IMAPRESOURCE_LOG) << "IDLE stats received:" << job << mailBox << messageCount << recentCount;
+    qCDebug(IMAPRESOURCE_LOG) << "Cached information:" << m_state->collection().remoteId() << m_state->collection().id()
+                              << m_lastMessageCount << m_lastRecentCount;
+
+    // It seems we're not in sync with the cache, resync is needed
+    if (messageCount != m_lastMessageCount || recentCount != m_lastRecentCount) {
+        m_lastMessageCount = messageCount;
+        m_lastRecentCount = recentCount;
+
+        qCDebug(IMAPRESOURCE_LOG) << "Resync needed for" << mailBox << m_state->collection().id();
+        m_resource->synchronizeCollection(m_state->collection().id());
+    }
+}
+
+void ImapIdleManager::onFlagsChanged(KIMAP::IdleJob *job)
+{
+    qCDebug(IMAPRESOURCE_LOG) << "IDLE flags changed in" << m_session->selectedMailBox();
+    m_resource->synchronizeCollection(m_state->collection().id());
+}
+
diff --git a/resources/imap/imapidlemanager.h b/resources/imap/imapidlemanager.h
new file mode 100644 (file)
index 0000000..4a2f514
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+    Copyright (c) 2007 Till Adam <adam@kde.org>
+    Copyright (C) 2008 Omat Holding B.V. <info@omat.nl>
+    Copyright (C) 2009 Kevin Ottens <ervin@kde.org>
+
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef RESOURCES_IMAP_IMAPIDLEMANAGER_H
+#define RESOURCES_IMAP_IMAPIDLEMANAGER_H
+
+#include "resourcestateinterface.h"
+
+#include <collection.h>
+
+#include <QObject>
+#include <QPointer>
+
+namespace KIMAP
+{
+class IdleJob;
+class Session;
+}
+
+class ImapResourceBase;
+class SessionPool;
+
+class KJob;
+
+class ImapIdleManager : public QObject
+{
+    Q_OBJECT
+
+public:
+    ImapIdleManager(ResourceStateInterface::Ptr state, SessionPool *pool, ImapResourceBase *parent);
+    ~ImapIdleManager();
+    void stop();
+
+    KIMAP::Session *session() const;
+
+private Q_SLOTS:
+    void onConnectionLost(KIMAP::Session *session);
+    void onPoolDisconnect();
+
+    void onSessionRequestDone(qint64 requestId, KIMAP::Session *session,
+                              int errorCode, const QString &errorString);
+    void onSelectDone(KJob *job);
+    void onIdleStopped();
+    void onStatsReceived(KIMAP::IdleJob *job, const QString &mailBox,
+                         int messageCount, int recentCount);
+    void onFlagsChanged(KIMAP::IdleJob *job);
+    void reconnect();
+
+private:
+    qint64 m_sessionRequestId;
+    SessionPool *m_pool;
+    KIMAP::Session *m_session;
+    QPointer<KIMAP::IdleJob> m_idle;
+    ImapResourceBase *m_resource;
+    ResourceStateInterface::Ptr m_state;
+    qint64 m_lastMessageCount;
+    qint64 m_lastRecentCount;
+};
+
+#endif
diff --git a/resources/imap/imapresource.cpp b/resources/imap/imapresource.cpp
new file mode 100644 (file)
index 0000000..87d79b4
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+    Copyright (c) 2007 Till Adam <adam@kde.org>
+    Copyright (C) 2008 Omat Holding B.V. <info@omat.nl>
+    Copyright (C) 2009 Kevin Ottens <ervin@kde.org>
+
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "imapresource.h"
+
+#include "setupserver.h"
+#include "settings.h"
+#include "sessionpool.h"
+#include "settingspasswordrequester.h"
+#include "sessionuiproxy.h"
+#include "tracer.h"
+#include <QIcon>
+
+#include <KWindowSystem>
+#include <KLocalizedString>
+
+ImapResource::ImapResource(const QString &id)
+    : ImapResourceBase(id)
+{
+    m_pool->setPasswordRequester(new SettingsPasswordRequester(this, m_pool));
+    m_pool->setSessionUiProxy(SessionUiProxy::Ptr(new SessionUiProxy));
+}
+
+ImapResource::~ImapResource()
+{
+}
+
+QString ImapResource::defaultName() const
+{
+    return i18n("IMAP Account");
+}
+
+QDialog *ImapResource::createConfigureDialog(WId windowId)
+{
+    SetupServer *dlg = new SetupServer(this, windowId);
+    KWindowSystem::setMainWindow(dlg, windowId);
+    dlg->setWindowIcon(QIcon::fromTheme(QStringLiteral("network-server")));
+    connect(dlg, &SetupServer::finished, this, &ImapResource::onConfigurationDone);
+    return dlg;
+}
+
+void ImapResource::onConfigurationDone(int result)
+{
+    SetupServer *dlg = qobject_cast<SetupServer *>(sender());
+    if (result) {
+        if (dlg->shouldClearCache()) {
+            clearCache();
+        }
+        settings()->save();
+    }
+    dlg->deleteLater();
+}
+
+void ImapResource::cleanup()
+{
+    settings()->cleanup();
+    Akonadi::AgentBase::cleanup();
+}
diff --git a/resources/imap/imapresource.desktop b/resources/imap/imapresource.desktop
new file mode 100644 (file)
index 0000000..23b0ea4
--- /dev/null
@@ -0,0 +1,101 @@
+[Desktop Entry]
+Name=IMAP E-Mail Server
+Name[bg]=Пощенски сървър IMAP
+Name[bs]=IMAP server za e-poštu
+Name[ca]=Servidor de correu IMAP
+Name[ca@valencia]=Servidor de correu IMAP
+Name[cs]=E-mailový IMAP server
+Name[da]=IMAP e-mail-server
+Name[de]=IMAP-E-Mail-Server
+Name[el]=Eξυπηρετητής ηλ.ταχυδρομείου IMAP
+Name[en_GB]=IMAP E-Mail Server
+Name[es]=Servidor de correo IMAP
+Name[et]=IMAP e-posti server
+Name[fi]=IMAP-sähköpostipalvelin
+Name[fr]=Serveur de courriers électroniques IMAP
+Name[ga]=Freastalaí Ríomhphoist IMAP
+Name[gl]=Servidor de Correo IMAP
+Name[hu]=IMAP e-mail kiszolgáló
+Name[ia]=Servitor IMAP de e-posta
+Name[it]=Server di posta IMAP
+Name[ja]=IMAP メールサーバ
+Name[kk]=IMAP эл.пошта сервері
+Name[km]=ម៉ាស៊ីន​បម្រើ​អ៊ីមែល IMAP
+Name[ko]=IMAP 이메일 서버
+Name[lt]=IMAP el. pašto serveris
+Name[lv]=IMAP e-pasta serveris
+Name[nb]=IMAP E-post-tjener
+Name[nds]=IMAP-Nettpostserver
+Name[nl]=IMAP e-mailserver
+Name[nn]=IMAP-basert e-posttenar
+Name[pa]=IMAP ਈਮੇਲ ਸਰਵਰ
+Name[pl]=Serwer poczty IMAP
+Name[pt]=Servidor de E-Mail IMAP
+Name[pt_BR]=Servidor de e-mails IMAP
+Name[ro]=Server de poștă IMAP
+Name[ru]=Почтовый сервер IMAP
+Name[sk]=IMAP poštový server
+Name[sl]=E-poštni strežnik IMAP
+Name[sr]=ИМАП сервер е‑поште
+Name[sr@ijekavian]=ИМАП сервер е‑поште
+Name[sr@ijekavianlatin]=IMAP server e‑pošte
+Name[sr@latin]=IMAP server e‑pošte
+Name[sv]=IMAP e-postserver
+Name[tr]=IMAP E-posta Sunucusu
+Name[uk]=Сервер пошти IMAP
+Name[x-test]=xxIMAP E-Mail Serverxx
+Name[zh_CN]=IMAP 邮件服务器
+Name[zh_TW]=IMAP 信件伺服器
+Comment=Connects to an IMAP e-mail server
+Comment[bg]=Свързване с пощенски сървър IMAP
+Comment[bs]=Povezivanje na IMAP e-mail server
+Comment[ca]=Connecta a un servidor de correu IMAP
+Comment[ca@valencia]=Connecta a un servidor de correu IMAP
+Comment[cs]=Připojí se na e-mailový IMAP server
+Comment[da]=Forbinder til en IMAP e-mail-server
+Comment[de]=Verbindet zu einem IMAP E-Mail-Server.
+Comment[el]=Συνδέεται σε έναν εξυπηρετητή ηλ.ταχυδρομείου IMAP
+Comment[en_GB]=Connects to an IMAP e-mail server
+Comment[es]=Conecta a un servidor de correo IMAP
+Comment[et]=Ühendumine IMAP e-posti serveriga
+Comment[fi]=Yhdistää IMAP-sähköpostipalvelimeen
+Comment[fr]=Se connecte à un serveur de courriers électroniques IMAP
+Comment[gl]=Conéctase a un servidor de correo IMAP
+Comment[hu]=Kapcsolódás egy IMAP e-mail kiszolgálóhoz
+Comment[ia]=Connecte a un servitor IMAP de e-posta
+Comment[it]=Si collega ad un server di posta IMAP
+Comment[ja]=IMAP のメールサーバに接続します。
+Comment[kk]=IMAP эл.пошта серверімен байланыс құру
+Comment[km]=តភ្ជាប់​ទៅ​កាន់​ម៉ាស៊ីន​បម្រើ​អ៊ីមែល IMAP
+Comment[ko]=IMAP 이메일 서버에 연결함
+Comment[lt]=Prisijungia prie IMAP el. pašto serverio.
+Comment[lv]=Pieslēdzas IMAP e-pasta serverim
+Comment[nb]=Kobler til en IMAP e-posttjener
+Comment[nds]=Stellt en Verbinnen na en IMAP-Nettpostserver op.
+Comment[nl]=Maakt verbinding met een IMAP-e-mailserver
+Comment[nn]=Koplar til ein IMAP-basert e-posttenar
+Comment[pa]=IMAP ਈਮੇਲ ਸਰਵਰ ਨਾਲ ਕੁਨੈਕਟ ਕਰਦਾ ਹੈ।
+Comment[pl]=Łączy z serwerem poczty IMAP
+Comment[pt]=Liga-se a um servidor de e-mail em IMAP
+Comment[pt_BR]=Conecta a um servidor de e-mails IMAP
+Comment[ro]=Se conectează la un server de poștă IMAP
+Comment[ru]=Подключение к почтовому серверу IMAP
+Comment[sk]=Pripojí sa na IMAP poštový server
+Comment[sl]=Poveže se z e-poštnim strežnikom IMAP
+Comment[sr]=Повезује се на ИМАП сервер е‑поште
+Comment[sr@ijekavian]=Повезује се на ИМАП сервер е‑поште
+Comment[sr@ijekavianlatin]=Povezuje se na IMAP server e‑pošte
+Comment[sr@latin]=Povezuje se na IMAP server e‑pošte
+Comment[sv]=Ansluter till en IMAP e-postserver
+Comment[tr]=IMAP e-posta sunucusuna bağlanır
+Comment[uk]=Встановлює з’єднання з поштовим сервером IMAP
+Comment[x-test]=xxConnects to an IMAP e-mail serverxx
+Comment[zh_CN]=连接到 IMAP 电子邮件服务器
+Comment[zh_TW]=連線到 IMAP 信件伺服器
+Type=AkonadiResource
+Exec=akonadi_imap_resource
+Icon=network-server
+
+X-Akonadi-MimeTypes=message/rfc822
+X-Akonadi-Capabilities=Resource
+X-Akonadi-Identifier=akonadi_imap_resource
diff --git a/resources/imap/imapresource.h b/resources/imap/imapresource.h
new file mode 100644 (file)
index 0000000..47c0194
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+    Copyright (c) 2007 Till Adam <adam@kde.org>
+    Copyright (C) 2008 Omat Holding B.V. <info@omat.nl>
+    Copyright (C) 2009 Kevin Ottens <ervin@kde.org>
+
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef IMAPRESOURCE_H
+#define IMAPRESOURCE_H
+
+#include <imapresourcebase.h>
+
+class ImapResource : public ImapResourceBase
+{
+    Q_OBJECT
+    Q_CLASSINFO("D-Bus Interface", "org.kde.Akonadi.Imap.Resource")
+
+public:
+    explicit ImapResource(const QString &id);
+    virtual ~ImapResource();
+
+    QDialog *createConfigureDialog(WId windowId) Q_DECL_OVERRIDE;
+    void cleanup() Q_DECL_OVERRIDE;
+
+protected:
+    QString defaultName() const Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void onConfigurationDone(int result);
+
+};
+
+#endif // IMAPRESOURCE_H
diff --git a/resources/imap/imapresource.kcfg b/resources/imap/imapresource.kcfg
new file mode 100644 (file)
index 0000000..b003e95
--- /dev/null
@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:kcfg="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
+      http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
+  <kcfgfile/>
+  <group name="network">
+    <entry name="ImapServer" type="String">
+      <label>IMAP server</label>
+    </entry>
+    <entry name="ImapPort"  type="Int">
+      <label>Defines the port the IMAP service is running on</label>
+      <default>993</default>
+    </entry>
+    <entry name="UserName" type="String">
+      <label>Username</label>
+    </entry>
+    <entry name="Safety" type="String">
+      <label>Defines the encryption type to use</label>
+      <default>SSL</default>
+    </entry>
+    <entry name="OverrideEncryption" type="String">
+      <label>Override configured encryption mode</label>
+    </entry>
+    <entry name="Authentication" type="Int">
+      <label>Defines the authentication type to use</label>
+      <default>1</default>
+    </entry>
+    <entry name="SubscriptionEnabled" type="Bool">
+      <label>Defines if the server side subscription is enabled</label>
+      <default>false</default>
+    </entry>
+    <entry name="SessionTimeout" type="Int">
+      <default>30</default>
+    </entry>
+  </group>
+  <group name="cache">
+    <entry name="DisconnectedModeEnabled" type="Bool">
+      <label>Defines if all the IMAP data should be cached locally all the time</label>
+      <default>false</default>
+    </entry>
+    <entry name="IntervalCheckEnabled" type="Bool">
+       <label>Defines if interval checking is enabled.</label>
+       <default>true</default>
+     </entry>
+    <entry name="IntervalCheckTime" type="Int">
+      <label>Check interval in minutes</label>
+      <default>5</default>
+    </entry>
+    <entry name="RetrieveMetadataOnFolderListing" type="Bool">
+      <label>Defines if the annotations, ACLs and quota information of mailboxes should
+             also be retrieved when the mailboxes get listed.</label>
+      <default>true</default>
+    </entry>
+    <entry name="AutomaticExpungeEnabled" type="Bool">
+      <label>Defines if the expunge command is issued automatically, otherwise it should be
+             triggered manually through the D-Bus interface.</label>
+      <default>true</default>
+    </entry>
+    <entry name="TrashCollection" type="LongLong">
+       <label>Define which folder is used for trash</label>
+       <default>-1</default>
+    </entry>
+    <entry name="TrashCollectionMigrated" type="Bool">
+       <label>Define if the trash collection received the special attribute</label>
+       <default>false</default>
+    </entry>
+    <entry name="UseDefaultIdentity" type="Bool">
+       <label>Define if account uses the default identity</label>
+       <default>true</default>
+    </entry>
+    <entry name="AccountIdentity" type="Int">
+       <label>Identity account</label>
+    </entry>
+    <entry name="KnownMailBoxes" type="StringList">
+      <label>List of mailbox names reported by the server the last time</label>
+    </entry>
+  </group>
+  <group name="idle">
+    <entry name="IdleRidPath" type="StringList">
+      <label>RID path to the mailbox to watch for changes</label>
+    </entry>
+  </group>
+  <group name="siever">
+    <entry name="SieveSupport" type="Bool">
+      <label>Define if server supports sieve</label>
+      <default>false</default>
+    </entry>
+    <entry name="SieveReuseConfig" type="Bool">
+      <label>Define if we reuse host and login configuration</label>
+      <default>true</default>
+    </entry>
+    <entry name="SievePort" type="Int">
+      <label>Define sieve port</label>
+      <default>4190</default>
+    </entry>
+    <entry name="SieveAlternateUrl" type="String">
+      <label>Define alternate URL</label>
+    </entry>
+    <entry name="AlternateAuthentication" type="Int">
+      <label>Defines the authentication type to use for alternate server</label>
+      <default>1</default>
+    </entry>
+    <entry name="SieveVacationFilename" type="String">
+      <label>Define default sieve vacation filename</label>
+      <default>kmail-vacation.siv</default>
+    </entry>
+    <entry name="SieveCustomUsername" type="String">
+      <label>Define username used from custom server sieve url</label>
+      <default></default>
+    </entry>
+    <entry name="SieveCustomAuthentification" type="String">
+      <label>Defines the type of identification used by custom sieve server</label>
+      <default>ImapUserPassword</default>
+    </entry>
+  </group>
+</kcfg>
diff --git a/resources/imap/imapresourcebase.cpp b/resources/imap/imapresourcebase.cpp
new file mode 100644 (file)
index 0000000..3b14045
--- /dev/null
@@ -0,0 +1,756 @@
+/*
+    Copyright (c) 2007 Till Adam <adam@kde.org>
+    Copyright (C) 2008 Omat Holding B.V. <info@omat.nl>
+    Copyright (C) 2009 Kevin Ottens <ervin@kde.org>
+
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "imapresourcebase.h"
+
+#include <QHostInfo>
+#include <QSettings>
+
+#include <QIcon>
+#include "imapresource_debug.h"
+#include <KLocalizedString>
+
+#include <kwindowsystem.h>
+#include <AkonadiCore/CollectionModifyJob>
+
+#include <AkonadiCore/agentmanager.h>
+#include <AkonadiCore/attributefactory.h>
+#include <AkonadiCore/collectionfetchjob.h>
+#include <AkonadiCore/collectionfetchscope.h>
+#include <AkonadiCore/changerecorder.h>
+#include <AkonadiCore/itemfetchscope.h>
+#include <AkonadiCore/itemfetchjob.h>
+#include <AkonadiCore/specialcollections.h>
+#include <AkonadiCore/session.h>
+#include <akonadi/kmime/messageparts.h>
+#include <QStandardPaths>
+
+#include "collectionannotationsattribute.h"
+#include "collectionflagsattribute.h"
+#include "imapaclattribute.h"
+#include "imapquotaattribute.h"
+#include "noselectattribute.h"
+#include "uidvalidityattribute.h"
+#include "uidnextattribute.h"
+#include "highestmodseqattribute.h"
+
+#include "settings.h"
+#include "imapaccount.h"
+#include "imapidlemanager.h"
+#include "resourcestate.h"
+#include "subscriptiondialog.h"
+
+#include "addcollectiontask.h"
+#include "additemtask.h"
+#include "changecollectiontask.h"
+#include "changeitemsflagstask.h"
+#include "changeitemtask.h"
+#include "expungecollectiontask.h"
+#include "movecollectiontask.h"
+#include "moveitemstask.h"
+#include "removecollectionrecursivetask.h"
+#include "retrievecollectionmetadatatask.h"
+#include "retrievecollectionstask.h"
+#include "retrieveitemtask.h"
+#include "retrieveitemstask.h"
+#include "searchtask.h"
+
+#include "settingspasswordrequester.h"
+#include "sessionpool.h"
+#include "sessionuiproxy.h"
+#include "imapflags.h"
+
+#include "resourceadaptor.h"
+
+#ifdef MAIL_SERIALIZER_PLUGIN_STATIC
+
+Q_IMPORT_PLUGIN(akonadi_serializer_mail)
+#endif
+
+Q_DECLARE_METATYPE(QList<qint64>)
+Q_DECLARE_METATYPE(QWeakPointer<QObject>)
+
+using namespace Akonadi;
+
+ImapResourceBase::ImapResourceBase(const QString &id)
+    : ResourceBase(id),
+      m_pool(new SessionPool(2, this)),
+      m_settings(Q_NULLPTR),
+      mSubscriptions(Q_NULLPTR),
+      m_idle(Q_NULLPTR)
+{
+    QTimer::singleShot(0, this, &ImapResourceBase::updateResourceName);
+
+    connect(m_pool, &SessionPool::connectDone,
+            this, &ImapResourceBase::onConnectDone);
+    connect(m_pool, &SessionPool::connectionLost,
+            this, &ImapResourceBase::onConnectionLost);
+
+    Akonadi::AttributeFactory::registerAttribute<UidValidityAttribute>();
+    Akonadi::AttributeFactory::registerAttribute<UidNextAttribute>();
+    Akonadi::AttributeFactory::registerAttribute<NoSelectAttribute>();
+    Akonadi::AttributeFactory::registerAttribute<HighestModSeqAttribute>();
+
+    Akonadi::AttributeFactory::registerAttribute<CollectionAnnotationsAttribute>();
+    Akonadi::AttributeFactory::registerAttribute<CollectionFlagsAttribute>();
+
+    Akonadi::AttributeFactory::registerAttribute<ImapAclAttribute>();
+    Akonadi::AttributeFactory::registerAttribute<ImapQuotaAttribute>();
+
+    // For QMetaObject::invokeMethod()
+    qRegisterMetaType<QList<qint64> >();
+
+    changeRecorder()->fetchCollection(true);
+    changeRecorder()->collectionFetchScope().setAncestorRetrieval(CollectionFetchScope::All);
+    changeRecorder()->collectionFetchScope().setIncludeStatistics(true);
+    changeRecorder()->itemFetchScope().fetchFullPayload(true);
+    changeRecorder()->itemFetchScope().setAncestorRetrieval(ItemFetchScope::All);
+    changeRecorder()->itemFetchScope().setFetchModificationTime(false);
+//(Andras) disable now, as tokoe reported problems with it and the mail filter: changeRecorder()->fetchChangedOnly( true );
+
+    setHierarchicalRemoteIdentifiersEnabled(true);
+    setItemTransactionMode(ItemSync::MultipleTransactions);   // we can recover from incomplete syncs, so we can use a faster mode
+    ItemFetchScope scope(changeRecorder()->itemFetchScope());
+    scope.fetchFullPayload(false);
+    scope.setAncestorRetrieval(ItemFetchScope::None);
+    setItemSynchronizationFetchScope(scope);
+    setDisableAutomaticItemDeliveryDone(true);
+    setItemSyncBatchSize(100);
+
+    connect(this, &AgentBase::reloadConfiguration, this, &ImapResourceBase::reconnect);
+
+    m_statusMessageTimer = new QTimer(this);
+    m_statusMessageTimer->setSingleShot(true);
+    connect(m_statusMessageTimer, &QTimer::timeout, this, &ImapResourceBase::clearStatusMessage);
+    connect(this, &AgentBase::error, this, &ImapResourceBase::showError);
+
+    QMetaObject::invokeMethod(this, "delayedInit", Qt::QueuedConnection);
+}
+
+void ImapResourceBase::delayedInit()
+{
+    settings(); // make sure the D-Bus settings interface is up
+    new ImapResourceBaseAdaptor(this);
+    setNeedsNetwork(needsNetwork());
+
+    // Migration issue: trash folder had ID in config, but didn't have SpecialCollections attribute, fix that.
+    if (!settings()->trashCollectionMigrated()) {
+        const Akonadi::Collection::Id trashCollection = settings()->trashCollection();
+        if (trashCollection != -1) {
+            Collection attributeCollection(trashCollection);
+            SpecialCollections::setSpecialCollectionType("trash", attributeCollection);
+        }
+        settings()->setTrashCollectionMigrated(true);
+    }
+}
+
+ImapResourceBase::~ImapResourceBase()
+{
+    //Destroy everything that could cause callbacks immediately, otherwise the callbacks can result in a crash.
+
+    if (m_idle) {
+        delete m_idle;
+        m_idle = Q_NULLPTR;
+    }
+
+    Q_FOREACH (ResourceTask *task, m_taskList) {
+        delete task;
+    }
+    m_taskList.clear();
+
+    delete m_pool;
+    delete m_settings;
+}
+
+void ImapResourceBase::aboutToQuit()
+{
+    //TODO the resource would ideally have to signal when it's done with logging out etc, before the destructor gets called
+    if (m_idle) {
+        m_idle->stop();
+    }
+
+    Q_FOREACH (ResourceTask *task, m_taskList) {
+        task->kill();
+    }
+
+    m_pool->disconnect();
+}
+
+void ImapResourceBase::updateResourceName()
+{
+    if (name() == identifier()) {
+        const QString agentType = AgentManager::self()->instance(identifier()).type().identifier();
+        const QString agentsrcFile = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char('/') + QLatin1String("akonadi/agentsrc");
+
+        const QSettings agentsrc(agentsrcFile, QSettings::IniFormat);
+        const int instanceCounter = agentsrc.value(
+                                        QStringLiteral("InstanceCounters/%1/InstanceCounter").arg(agentType),
+                                        -1).toInt();
+
+        if (instanceCounter > 0) {
+            setName(QStringLiteral("%1 %2").arg(defaultName()).arg(instanceCounter));
+        } else {
+            setName(defaultName());
+        }
+    }
+}
+
+// -----------------------------------------------------------------------------
+
+void ImapResourceBase::configure(WId windowId)
+{
+    if (createConfigureDialog(windowId)->exec() == QDialog::Accepted) {
+        Q_EMIT configurationDialogAccepted();
+        reconnect();
+    } else {
+        Q_EMIT configurationDialogRejected();
+    }
+}
+
+// ----------------------------------------------------------------------------------
+
+void ImapResourceBase::startConnect(const QVariant &)
+{
+    if (settings()->imapServer().isEmpty()) {
+        setOnline(false);
+        Q_EMIT status(NotConfigured, i18n("No server configured yet."));
+        taskDone();
+        return;
+    }
+
+    m_pool->disconnect(); // reset all state, delete any old account
+    ImapAccount *account = new ImapAccount;
+    settings()->loadAccount(account);
+
+    const bool result = m_pool->connect(account);
+    Q_ASSERT(result);
+    Q_UNUSED(result);
+}
+
+int ImapResourceBase::configureSubscription(qlonglong windowId)
+{
+    if (mSubscriptions) {
+        return 0;
+    }
+
+    if (!m_pool->account()) {
+        return -2;
+    }
+    const QString password = settings()->password();
+    if (password.isEmpty()) {
+        return -1;
+    }
+
+    mSubscriptions = new SubscriptionDialog(Q_NULLPTR, SubscriptionDialog::AllowToEnableSubscription);
+    if (windowId) {
+#ifndef Q_OS_WIN
+        KWindowSystem::setMainWindow(mSubscriptions, windowId);
+#else
+        KWindowSystem::setMainWindow(mSubscriptions, (HWND)windowId);
+#endif
+    }
+    mSubscriptions->setWindowTitle(i18nc("@title:window", "Serverside Subscription"));
+    mSubscriptions->setWindowIcon(QIcon::fromTheme(QStringLiteral("network-server")));
+    mSubscriptions->connectAccount(*m_pool->account(), password);
+    mSubscriptions->setSubscriptionEnabled(settings()->subscriptionEnabled());
+
+    if (mSubscriptions->exec()) {
+        settings()->setSubscriptionEnabled(mSubscriptions->subscriptionEnabled());
+        settings()->save();
+        Q_EMIT configurationDialogAccepted();
+        reconnect();
+    }
+    delete mSubscriptions;
+
+    return 0;
+}
+
+void ImapResourceBase::onConnectDone(int errorCode, const QString &errorString)
+{
+    switch (errorCode) {
+    case SessionPool::NoError:
+        setOnline(true);
+        taskDone();
+        Q_EMIT status(Idle, i18n("Connection established."));
+
+        synchronizeCollectionTree();
+        break;
+
+    case SessionPool::PasswordRequestError:
+    case SessionPool::EncryptionError:
+    case SessionPool::LoginFailError:
+    case SessionPool::CapabilitiesTestError:
+    case SessionPool::IncompatibleServerError:
+        setOnline(false);
+        Q_EMIT status(Broken, errorString);
+        cancelTask();
+        return;
+
+    case SessionPool::CouldNotConnectError:
+        Q_EMIT status(Idle, i18n("Server is not available."));
+        deferTask();
+        setTemporaryOffline((m_pool->account() && m_pool->account()->timeout() > 0) ? m_pool->account()->timeout() : 300);
+        return;
+
+    case SessionPool::ReconnectNeededError:
+        reconnect();
+        return;
+
+    case SessionPool::NoAvailableSessionError:
+        qFatal("Shouldn't happen");
+        return;
+    case SessionPool::CancelledError:
+        qCWarning(IMAPRESOURCE_LOG) << "Session login cancelled";
+        return;
+    }
+}
+
+void ImapResourceBase::onConnectionLost(KIMAP::Session */*session*/)
+{
+    if (!m_pool->isConnected()) {
+        reconnect();
+    }
+}
+
+ResourceStateInterface::Ptr ImapResourceBase::createResourceState(const TaskArguments &args)
+{
+    return ResourceStateInterface::Ptr(new ResourceState(this, args));
+}
+
+Settings *ImapResourceBase::settings() const
+{
+    if (m_settings == Q_NULLPTR) {
+        m_settings = new Settings;
+    }
+
+    return m_settings;
+}
+
+// ----------------------------------------------------------------------------------
+
+bool ImapResourceBase::retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts)
+{
+    // The collection name is empty here...
+    //emit status( AgentBase::Running, i18nc( "@info:status", "Retrieving item in '%1'", item.parentCollection().name() ) );
+
+    RetrieveItemTask *task = new RetrieveItemTask(createResourceState(TaskArguments(item, parts)), this);
+    task->start(m_pool);
+    queueTask(task);
+    return true;
+}
+
+void ImapResourceBase::itemAdded(const Item &item, const Collection &collection)
+{
+    Q_EMIT status(AgentBase::Running, i18nc("@info:status", "Adding item in '%1'", collection.name()));
+
+    startTask(new AddItemTask(createResourceState(TaskArguments(item, collection)), this));
+}
+
+void ImapResourceBase::itemChanged(const Item &item, const QSet<QByteArray> &parts)
+{
+    Q_EMIT status(AgentBase::Running, i18nc("@info:status", "Updating item in '%1'", item.parentCollection().name()));
+
+    startTask(new ChangeItemTask(createResourceState(TaskArguments(item, parts)), this));
+}
+
+void ImapResourceBase::itemsFlagsChanged(const Item::List &items, const QSet< QByteArray > &addedFlags,
+        const QSet< QByteArray > &removedFlags)
+{
+    Q_EMIT status(AgentBase::Running, i18nc("@info:status", "Updating items"));
+
+    startTask(new ChangeItemsFlagsTask(createResourceState(TaskArguments(items, addedFlags, removedFlags)), this));
+}
+
+void ImapResourceBase::itemsRemoved(const Akonadi::Item::List &items)
+{
+    const QString mailBox = ResourceStateInterface::mailBoxForCollection(items.first().parentCollection(), false);
+    if (mailBox.isEmpty()) {
+        // this item will be removed soon by its parent collection
+        changeProcessed();
+        return;
+    }
+
+    Q_EMIT status(AgentBase::Running, i18nc("@info:status", "Removing items"));
+
+    startTask(new ChangeItemsFlagsTask(createResourceState(TaskArguments(items, QSet<QByteArray>() << ImapFlags::Deleted, QSet<QByteArray>())), this));
+}
+
+void ImapResourceBase::itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &source,
+                                  const Akonadi::Collection &destination)
+{
+    if (items.first().parentCollection() != destination) {   // should have been set by the server
+        qCWarning(IMAPRESOURCE_LOG) << "Collections don't match: destination=" << destination.id()
+                                    << "; items parent=" << items.first().parentCollection().id()
+                                    << "; source collection=" << source.id();
+        //Q_ASSERT( false );
+        //TODO: Find out why this happens
+        cancelTask();
+        return;
+    }
+
+    Q_EMIT status(AgentBase::Running, i18nc("@info:status", "Moving items from '%1' to '%2'", source.name(), destination.name()));
+
+    startTask(new MoveItemsTask(createResourceState(TaskArguments(items, source, destination)), this));
+}
+
+// ----------------------------------------------------------------------------------
+
+void ImapResourceBase::retrieveCollections()
+{
+    Q_EMIT status(AgentBase::Running, i18nc("@info:status", "Retrieving folders"));
+
+    startTask(new RetrieveCollectionsTask(createResourceState(TaskArguments()), this));
+}
+
+void ImapResourceBase::retrieveCollectionAttributes(const Akonadi::Collection &col)
+{
+    Q_EMIT status(AgentBase::Running, i18nc("@info:status", "Retrieving extra folder information for '%1'", col.name()));
+    startTask(new RetrieveCollectionMetadataTask(createResourceState(TaskArguments(col)), this));
+}
+
+void ImapResourceBase::retrieveItems(const Collection &col)
+{
+    synchronizeCollectionAttributes(col.id());
+
+    setItemStreamingEnabled(true);
+
+    RetrieveItemsTask *task = new RetrieveItemsTask(createResourceState(TaskArguments(col)), this);
+    connect(task, SIGNAL(status(int,QString)), SIGNAL(status(int,QString)));
+    connect(this, &ResourceBase::retrieveNextItemSyncBatch, task, &RetrieveItemsTask::onReadyForNextBatch);
+    startTask(task);
+
+}
+
+void ImapResourceBase::collectionAdded(const Collection &collection, const Collection &parent)
+{
+    Q_EMIT status(AgentBase::Running, i18nc("@info:status", "Creating folder '%1'", collection.name()));
+    startTask(new AddCollectionTask(createResourceState(TaskArguments(collection, parent)), this));
+}
+
+void ImapResourceBase::collectionChanged(const Collection &collection, const QSet<QByteArray> &parts)
+{
+    Q_EMIT status(AgentBase::Running, i18nc("@info:status", "Updating folder '%1'", collection.name()));
+    startTask(new ChangeCollectionTask(createResourceState(TaskArguments(collection, parts)), this));
+}
+
+void ImapResourceBase::collectionRemoved(const Collection &collection)
+{
+    //TODO Move this to the task
+    const QString mailBox = ResourceStateInterface::mailBoxForCollection(collection, false);
+    if (mailBox.isEmpty()) {
+        // this collection will be removed soon by its parent collection
+        changeProcessed();
+        return;
+    }
+    Q_EMIT status(AgentBase::Running, i18nc("@info:status", "Removing folder '%1'", collection.name()));
+
+    startTask(new RemoveCollectionRecursiveTask(createResourceState(TaskArguments(collection)), this));
+}
+
+void ImapResourceBase::collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &source,
+                                       const Akonadi::Collection &destination)
+{
+    Q_EMIT status(AgentBase::Running, i18nc("@info:status", "Moving folder '%1' from '%2' to '%3'",
+                                            collection.name(), source.name(), destination.name()));
+    startTask(new MoveCollectionTask(createResourceState(TaskArguments(collection, source, destination)), this));
+}
+
+void ImapResourceBase::addSearch(const QString &query, const QString &queryLanguage, const Collection &resultCollection)
+{
+}
+
+void ImapResourceBase::removeSearch(const Collection &resultCollection)
+{
+}
+
+void ImapResourceBase::search(const QString &query, const Collection &collection)
+{
+    QVariantMap arg;
+    arg[QStringLiteral("query")] = query;
+    arg[QStringLiteral("collection")] = QVariant::fromValue(collection);
+    scheduleCustomTask(this, "doSearch", arg);
+}
+
+void ImapResourceBase::doSearch(const QVariant &arg)
+{
+    const QVariantMap map = arg.toMap();
+    const QString query = map[QStringLiteral("query")].toString();
+    const Collection collection = map[QStringLiteral("collection")].value<Collection>();
+
+    Q_EMIT status(AgentBase::Running, i18nc("@info:status", "Searching..."));
+    startTask(new SearchTask(createResourceState(TaskArguments(collection)), query, this));
+}
+
+// -----
+
+// ----------------------------------------------------------------------------------
+
+void ImapResourceBase::scheduleConnectionAttempt()
+{
+    // block all other tasks, until we are connected
+    scheduleCustomTask(this, "startConnect", QVariant(), ResourceBase::Prepend);
+}
+
+void ImapResourceBase::doSetOnline(bool online)
+{
+    qCDebug(IMAPRESOURCE_LOG) << "online=" << online;
+    if (!online) {
+        Q_FOREACH (ResourceTask *task, m_taskList) {
+            task->kill();
+            delete task;
+        }
+        m_taskList.clear();
+        m_pool->cancelPasswordRequests();
+        if (m_pool->isConnected()) {
+            m_pool->disconnect();
+        }
+        if (m_idle) {
+            m_idle->stop();
+            delete m_idle;
+            m_idle = Q_NULLPTR;
+        }
+        settings()->clearCachedPassword();
+    } else if (online && !m_pool->isConnected()) {
+        scheduleConnectionAttempt();
+    }
+    ResourceBase::doSetOnline(online);
+}
+
+QChar ImapResourceBase::separatorCharacter() const
+{
+    return m_separatorCharacter;
+}
+
+void ImapResourceBase::setSeparatorCharacter(const QChar &separator)
+{
+    m_separatorCharacter = separator;
+}
+
+bool ImapResourceBase::needsNetwork() const
+{
+    const QString hostName = settings()->imapServer().section(QLatin1Char(':'), 0, 0);
+    // ### is there a better way to do this?
+    if (hostName == QLatin1String("127.0.0.1") ||
+            hostName == QLatin1String("localhost") ||
+            hostName == QHostInfo::localHostName()) {
+        return false;
+    }
+    return true;
+}
+
+void ImapResourceBase::reconnect()
+{
+    setNeedsNetwork(needsNetwork());
+    setOnline(false);   // we are not connected initially
+    setOnline(true);
+}
+
+// ----------------------------------------------------------------------------------
+
+void ImapResourceBase::startIdleIfNeeded()
+{
+    if (!m_idle) {
+        startIdle();
+    }
+}
+
+void ImapResourceBase::startIdle()
+{
+    delete m_idle;
+    m_idle = Q_NULLPTR;
+
+    if (!m_pool->serverCapabilities().contains(QStringLiteral("IDLE"))) {
+        return;
+    }
+
+    //Without password we don't even have to try
+    if (settings()->password().isEmpty()) {
+        return;
+    }
+
+    const QStringList ridPath = settings()->idleRidPath();
+    if (ridPath.size() < 2) {
+        return;
+    }
+
+    Collection c, p;
+    p.setParentCollection(Collection::root());
+    for (int i = ridPath.size() - 1; i > 0; --i) {
+        p.setRemoteId(ridPath.at(i));
+        c.setParentCollection(p);
+        p = c;
+    }
+    c.setRemoteId(ridPath.first());
+
+    Akonadi::CollectionFetchScope scope;
+    scope.setResource(identifier());
+    scope.setAncestorRetrieval(Akonadi::CollectionFetchScope::All);
+
+    Akonadi::CollectionFetchJob *fetch
+        = new Akonadi::CollectionFetchJob(c, Akonadi::CollectionFetchJob::Base, this);
+    fetch->setFetchScope(scope);
+
+    connect(fetch, &KJob::result,
+            this, &ImapResourceBase::onIdleCollectionFetchDone);
+}
+
+void ImapResourceBase::onIdleCollectionFetchDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "CollectionFetch for idling failed."
+                                    << "error=" << job->error()
+                                    << ", errorString=" << job->errorString();
+        return;
+    }
+    Akonadi::CollectionFetchJob *fetch = static_cast<Akonadi::CollectionFetchJob *>(job);
+    //Can be empty if collection is not subscribed locally
+    if (!fetch->collections().isEmpty()) {
+        delete m_idle;
+        m_idle = new ImapIdleManager(createResourceState(TaskArguments(fetch->collections().at(0))), m_pool, this);
+    }
+}
+
+// ----------------------------------------------------------------------------------
+
+void ImapResourceBase::requestManualExpunge(qint64 collectionId)
+{
+    if (!settings()->automaticExpungeEnabled()) {
+        Collection collection(collectionId);
+
+        Akonadi::CollectionFetchScope scope;
+        scope.setResource(identifier());
+        scope.setAncestorRetrieval(Akonadi::CollectionFetchScope::All);
+        scope.setListFilter(CollectionFetchScope::NoFilter);
+
+        Akonadi::CollectionFetchJob *fetch
+            = new Akonadi::CollectionFetchJob(collection,
+                                              Akonadi::CollectionFetchJob::Base,
+                                              this);
+        fetch->setFetchScope(scope);
+
+        connect(fetch, &KJob::result,
+                this, &ImapResourceBase::onExpungeCollectionFetchDone);
+    }
+}
+
+void ImapResourceBase::onExpungeCollectionFetchDone(KJob *job)
+{
+    if (job->error() == 0) {
+        Akonadi::CollectionFetchJob *fetch = static_cast<Akonadi::CollectionFetchJob *>(job);
+        Akonadi::Collection collection = fetch->collections().at(0);
+
+        scheduleCustomTask(this, "triggerCollectionExpunge",
+                           QVariant::fromValue(collection));
+
+    } else {
+        qCWarning(IMAPRESOURCE_LOG) << "CollectionFetch for expunge failed."
+                                    << "error=" << job->error()
+                                    << ", errorString=" << job->errorString();
+    }
+}
+
+void ImapResourceBase::triggerCollectionExpunge(const QVariant &collectionVariant)
+{
+    const Collection collection = collectionVariant.value<Collection>();
+
+    ExpungeCollectionTask *task = new ExpungeCollectionTask(createResourceState(TaskArguments(collection)), this);
+    task->start(m_pool);
+    queueTask(task);
+}
+
+// ----------------------------------------------------------------------------------
+
+void ImapResourceBase::abortActivity()
+{
+    if (!m_taskList.isEmpty()) {
+        m_pool->disconnect(SessionPool::CloseSession);
+        scheduleConnectionAttempt();
+    }
+}
+
+void ImapResourceBase::queueTask(ResourceTask *task)
+{
+    connect(task, &QObject::destroyed,
+            this, &ImapResourceBase::taskDestroyed);
+    m_taskList << task;
+}
+
+void ImapResourceBase::startTask(ResourceTask *task)
+{
+    task->start(m_pool);
+    queueTask(task);
+}
+
+void ImapResourceBase::taskDestroyed(QObject *task)
+{
+    m_taskList.removeAll(static_cast<ResourceTask *>(task));
+}
+
+QStringList ImapResourceBase::serverCapabilities() const
+{
+    return m_pool->serverCapabilities();
+}
+
+void ImapResourceBase::cleanup()
+{
+    settings()->cleanup();
+}
+
+QString ImapResourceBase::dumpResourceToString() const
+{
+    QString ret;
+    Q_FOREACH (ResourceTask *task, m_taskList) {
+        if (!ret.isEmpty()) {
+            ret += QLatin1String(", ");
+        }
+        ret += QLatin1String(task->metaObject()->className());
+    }
+    return QLatin1String("IMAP tasks: ") + ret;
+}
+
+void ImapResourceBase::showError(const QString &message)
+{
+    Q_EMIT status(Akonadi::AgentBase::Idle, message);
+    m_statusMessageTimer->start(1000 * 10);
+}
+
+void ImapResourceBase::clearStatusMessage()
+{
+    Q_EMIT status(Akonadi::AgentBase::Idle, QString());
+}
+
+void ImapResourceBase::modifyCollection(const Collection &col)
+{
+    Akonadi::CollectionModifyJob *modJob = new Akonadi::CollectionModifyJob(col, this);
+    connect(modJob, &KJob::result, this, &ImapResourceBase::onCollectionModifyDone);
+}
+
+void ImapResourceBase::onCollectionModifyDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Failed to modify collection: " << job->errorString();
+    }
+}
+
diff --git a/resources/imap/imapresourcebase.h b/resources/imap/imapresourcebase.h
new file mode 100644 (file)
index 0000000..d5f223d
--- /dev/null
@@ -0,0 +1,169 @@
+/*
+    Copyright (c) 2007 Till Adam <adam@kde.org>
+    Copyright (C) 2008 Omat Holding B.V. <info@omat.nl>
+    Copyright (C) 2009 Kevin Ottens <ervin@kde.org>
+
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef RESOURCES_IMAP_IMAPRESOURCEBASE_H
+#define RESOURCES_IMAP_IMAPRESOURCEBASE_H
+
+#include <AkonadiAgentBase/resourcebase.h>
+#include <AkonadiAgentBase/agentsearchinterface.h>
+#include <QDialog>
+#include <QPointer>
+#include "resourcestateinterface.h"
+#include "resourcestate.h"
+
+class QTimer;
+
+class ResourceTask;
+namespace KIMAP
+{
+class Session;
+}
+
+class ImapIdleManager;
+class SessionPool;
+class ResourceState;
+class SubscriptionDialog;
+class Settings;
+
+class ImapResourceBase : public Akonadi::ResourceBase,
+    public Akonadi::AgentBase::ObserverV4,
+    public Akonadi::AgentSearchInterface
+{
+    Q_OBJECT
+    Q_CLASSINFO("D-Bus Interface", "org.kde.Akonadi.ImapResourceBase")
+protected:
+    using Akonadi::AgentBase::Observer::collectionChanged;
+
+public:
+    explicit ImapResourceBase(const QString &id);
+    ~ImapResourceBase();
+
+    virtual QDialog *createConfigureDialog(WId windowId) = 0;
+
+    void cleanup() Q_DECL_OVERRIDE;
+
+    virtual Settings *settings() const;
+
+public Q_SLOTS:
+    void configure(WId windowId) Q_DECL_OVERRIDE;
+
+    // DBus methods
+    Q_SCRIPTABLE void requestManualExpunge(qint64 collectionId);
+    Q_SCRIPTABLE int configureSubscription(qlonglong windowId = 0);
+    Q_SCRIPTABLE QStringList serverCapabilities() const;
+
+    // pseudo-virtual called by ResourceBase
+    QString dumpResourceToString() const Q_DECL_OVERRIDE;
+
+protected Q_SLOTS:
+    void startIdleIfNeeded();
+    void startIdle();
+
+    void abortActivity() Q_DECL_OVERRIDE;
+
+    void retrieveCollections() Q_DECL_OVERRIDE;
+    void retrieveCollectionAttributes(const Akonadi::Collection &col) Q_DECL_OVERRIDE;
+
+    void retrieveItems(const Akonadi::Collection &col) Q_DECL_OVERRIDE;
+    bool retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+
+protected:
+    void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    void itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+    void itemsFlagsChanged(const Akonadi::Item::List &items, const QSet<QByteArray> &addedFlags, const QSet<QByteArray> &removedFlags) Q_DECL_OVERRIDE;
+    void itemsRemoved(const Akonadi::Item::List &items) Q_DECL_OVERRIDE;
+    virtual void itemsMoved(const Akonadi::Item::List &item, const Akonadi::Collection &source,
+                            const Akonadi::Collection &destination) Q_DECL_OVERRIDE;
+
+    void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) Q_DECL_OVERRIDE;
+    void collectionChanged(const Akonadi::Collection &collection, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+    void collectionRemoved(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    virtual void collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &source,
+                                 const Akonadi::Collection &destination) Q_DECL_OVERRIDE;
+
+    void addSearch(const QString &query, const QString &queryLanguage, const Akonadi::Collection &resultCollection) Q_DECL_OVERRIDE;
+    void removeSearch(const Akonadi::Collection &resultCollection) Q_DECL_OVERRIDE;
+    void search(const QString &query, const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+
+    void doSetOnline(bool online) Q_DECL_OVERRIDE;
+
+    QChar separatorCharacter() const;
+    void setSeparatorCharacter(const QChar &separator);
+
+    void aboutToQuit() Q_DECL_OVERRIDE;
+
+    virtual ResourceStateInterface::Ptr createResourceState(const TaskArguments &);
+    virtual QString defaultName() const = 0;
+
+protected Q_SLOTS:
+    void delayedInit();
+
+private Q_SLOTS:
+    void doSearch(const QVariant &arg);
+
+    void reconnect();
+
+    void scheduleConnectionAttempt();
+    void startConnect(const QVariant &);   // the parameter is necessary, since this method is used by the task scheduler
+    void onConnectDone(int errorCode, const QString &errorMessage);
+    void onConnectionLost(KIMAP::Session *session);
+
+    void onIdleCollectionFetchDone(KJob *job);
+
+    void onExpungeCollectionFetchDone(KJob *job);
+    void triggerCollectionExpunge(const QVariant &collectionVariant);
+
+    void taskDestroyed(QObject *task);
+
+    void showError(const QString &message);
+    void clearStatusMessage();
+
+    void updateResourceName();
+
+    void onCollectionModifyDone(KJob *job);
+
+protected:
+    //Starts and queues a task
+    void startTask(ResourceTask *task);
+    void queueTask(ResourceTask *task);
+    SessionPool *m_pool;
+    mutable Settings *m_settings;
+
+private:
+    friend class ResourceState;
+
+    bool needsNetwork() const;
+    void modifyCollection(const Akonadi::Collection &);
+
+    friend class ImapIdleManager;
+
+    QList<ResourceTask *> m_taskList; //used to be able to kill tasks
+    QPointer<SubscriptionDialog> mSubscriptions;
+    ImapIdleManager *m_idle;
+    QTimer *m_statusMessageTimer;
+    QChar m_separatorCharacter;
+};
+
+#endif
diff --git a/resources/imap/main.cpp b/resources/imap/main.cpp
new file mode 100644 (file)
index 0000000..406837e
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "imapresource.h"
+
+AKONADI_RESOURCE_MAIN(ImapResource)
diff --git a/resources/imap/messagehelper.cpp b/resources/imap/messagehelper.cpp
new file mode 100644 (file)
index 0000000..4686a49
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+#include "messagehelper.h"
+
+#include <Akonadi/KMime/MessageFlags>
+
+#include "resourcetask.h"
+
+#include "imapresource_debug.h"
+
+MessageHelper::~MessageHelper()
+{
+
+}
+
+Akonadi::Item MessageHelper::createItemFromMessage(KMime::Message::Ptr message,
+        const qint64 uid,
+        const qint64 size,
+        const QList<KIMAP::MessageAttribute> &attrs,
+        const QList<QByteArray> &flags,
+        const KIMAP::FetchJob::FetchScope &scope,
+        bool &ok) const
+{
+    Q_UNUSED(attrs);
+
+    Akonadi::Item i;
+    if (scope.mode == KIMAP::FetchJob::FetchScope::Flags) {
+        i.setRemoteId(QString::number(uid));
+        i.setMimeType(KMime::Message::mimeType());
+        i.setFlags(Akonadi::Item::Flags::fromList(ResourceTask::toAkonadiFlags(flags)));
+    } else {
+        if (!message) {
+            qCWarning(IMAPRESOURCE_LOG) << "Got empty message: " << uid;
+            ok = false;
+            return Akonadi::Item();
+        }
+        // Sometimes messages might not have a body at all
+        if (message->body().isEmpty() && (scope.mode == KIMAP::FetchJob::FetchScope::Full || scope.mode == KIMAP::FetchJob::FetchScope::Content)) {
+            // In that case put a space in as body so that it gets cached
+            // otherwise we'll wrongly believe the body part is missing from the cache
+            message->setBody(" ");
+        }
+        i.setRemoteId(QString::number(uid));
+        i.setMimeType(KMime::Message::mimeType());
+        i.setPayload(KMime::Message::Ptr(message));
+        i.setSize(size);
+
+        Akonadi::MessageFlags::copyMessageFlags(*message, i);
+
+        foreach (const QByteArray &flag, ResourceTask::toAkonadiFlags(flags)) {
+            i.setFlag(flag);
+        }
+    }
+    ok = true;
+    return i;
+}
diff --git a/resources/imap/messagehelper.h b/resources/imap/messagehelper.h
new file mode 100644 (file)
index 0000000..6920267
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef MESSAGEHELPER_H
+#define MESSAGEHELPER_H
+
+#include <AkonadiCore/Item>
+#include <KMime/Message>
+#include <kimap/FetchJob>
+
+class MessageHelper
+{
+public:
+    typedef QSharedPointer<MessageHelper> Ptr;
+
+    virtual ~MessageHelper();
+    virtual Akonadi::Item createItemFromMessage(KMime::Message::Ptr message,
+            const qint64 uid,
+            const qint64 size,
+            const QList<KIMAP::MessageAttribute> &attrs,
+            const QList<QByteArray> &flags,
+            const KIMAP::FetchJob::FetchScope &scope,
+            bool &ok) const;
+};
+
+#endif
diff --git a/resources/imap/movecollectiontask.cpp b/resources/imap/movecollectiontask.cpp
new file mode 100644 (file)
index 0000000..ef811bc
--- /dev/null
@@ -0,0 +1,149 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "movecollectiontask.h"
+
+#include "imapresource_debug.h"
+#include <KLocalizedString>
+
+#include <kimap/renamejob.h>
+#include <kimap/session.h>
+#include <kimap/subscribejob.h>
+#include <kimap/selectjob.h>
+
+#include <QtCore/QUuid>
+
+MoveCollectionTask::MoveCollectionTask(const ResourceStateInterface::Ptr &resource, QObject *parent)
+    : ResourceTask(DeferIfNoSession, resource, parent)
+{
+
+}
+
+MoveCollectionTask::~MoveCollectionTask()
+{
+}
+
+void MoveCollectionTask::doStart(KIMAP::Session *session)
+{
+    if (collection().remoteId().isEmpty()) {
+        emitError(i18n("Cannot move IMAP folder '%1', it does not exist on the server.",
+                       collection().name()));
+        changeProcessed();
+        return;
+    }
+
+    if (sourceCollection().remoteId().isEmpty()) {
+        emitError(i18n("Cannot move IMAP folder '%1' out of '%2', '%2' does not exist on the server.",
+                       collection().name(),
+                       sourceCollection().name()));
+        changeProcessed();
+        return;
+    }
+
+    if (targetCollection().remoteId().isEmpty()) {
+        emitError(i18n("Cannot move IMAP folder '%1' to '%2', '%2' does not exist on the server.",
+                       collection().name(),
+                       sourceCollection().name()));
+        changeProcessed();
+        return;
+    }
+
+    if (session->selectedMailBox() != mailBoxForCollection(collection())) {
+        doRename(session);
+        return;
+    }
+
+    // Some IMAP servers don't allow moving an opened mailbox, so make sure
+    // it's not opened (https://bugs.kde.org/show_bug.cgi?id=324932) by examining
+    // a non-existent mailbox. We don't use CLOSE in order not to trigger EXPUNGE
+    KIMAP::SelectJob *examine = new KIMAP::SelectJob(session);
+    examine->setOpenReadOnly(true);   // use EXAMINE instead of SELECT
+    examine->setMailBox(QStringLiteral("IMAP Resource non existing folder %1").arg(QUuid::createUuid().toString()));
+    connect(examine, &KIMAP::SelectJob::result, this, &MoveCollectionTask::onExamineDone);
+    examine->start();
+}
+
+void MoveCollectionTask::onExamineDone(KJob *job)
+{
+    // We deliberately ignore any error here, because the SelectJob will always fail
+    // when examining a non-existent mailbox
+
+    KIMAP::SelectJob *examine = static_cast<KIMAP::SelectJob *>(job);
+    doRename(examine->session());
+}
+
+QString MoveCollectionTask::mailBoxForCollections(const Akonadi::Collection &parent, const Akonadi::Collection &child) const
+{
+    const QString parentMailbox = mailBoxForCollection(parent);
+    if (parentMailbox.isEmpty()) {
+        return child.remoteId().mid(1); //Strip separator on toplevel mailboxes
+    }
+    return parentMailbox + child.remoteId();
+}
+
+void MoveCollectionTask::doRename(KIMAP::Session *session)
+{
+    // collection.remoteId() already includes the separator
+    const QString oldMailBox = mailBoxForCollections(sourceCollection(), collection());
+    const QString newMailBox = mailBoxForCollections(targetCollection(), collection());
+
+    if (oldMailBox != newMailBox) {
+        KIMAP::RenameJob *job = new KIMAP::RenameJob(session);
+        job->setSourceMailBox(oldMailBox);
+        job->setDestinationMailBox(newMailBox);
+
+        connect(job, &KIMAP::RenameJob::result, this, &MoveCollectionTask::onRenameDone);
+
+        job->start();
+
+    } else {
+        changeProcessed();
+    }
+}
+
+void MoveCollectionTask::onRenameDone(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(job->errorString());
+    } else {
+        // Automatically subscribe to the new mailbox name
+        KIMAP::RenameJob *rename = static_cast<KIMAP::RenameJob *>(job);
+
+        KIMAP::SubscribeJob *subscribe = new KIMAP::SubscribeJob(rename->session());
+        subscribe->setMailBox(rename->destinationMailBox());
+
+        connect(subscribe, &KIMAP::SubscribeJob::result, this, &MoveCollectionTask::onSubscribeDone);
+
+        subscribe->start();
+    }
+}
+
+void MoveCollectionTask::onSubscribeDone(KJob *job)
+{
+    if (job->error()) {
+        emitWarning(i18n("Failed to subscribe to the folder '%1' on the IMAP server. "
+                         "It will disappear on next sync. Use the subscription dialog to overcome that",
+                         collection().name()));
+    }
+
+    changeCommitted(collection());
+}
+
diff --git a/resources/imap/movecollectiontask.h b/resources/imap/movecollectiontask.h
new file mode 100644 (file)
index 0000000..eba1b42
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef MOVECOLLECTIONTASK_H
+#define MOVECOLLECTIONTASK_H
+
+#include "resourcetask.h"
+
+class MoveCollectionTask : public ResourceTask
+{
+    Q_OBJECT
+
+public:
+    explicit MoveCollectionTask(const ResourceStateInterface::Ptr &resource, QObject *parent = Q_NULLPTR);
+    virtual ~MoveCollectionTask();
+
+private Q_SLOTS:
+    void onExamineDone(KJob *job);
+    void onRenameDone(KJob *job);
+    void onSubscribeDone(KJob *job);
+
+protected:
+    void doStart(KIMAP::Session *session) Q_DECL_OVERRIDE;
+
+private:
+    void doRename(KIMAP::Session *session);
+    QString mailBoxForCollections(const Akonadi::Collection &parent, const Akonadi::Collection &child) const;
+
+    Akonadi::Collection m_collection;
+};
+
+#endif
diff --git a/resources/imap/moveitemstask.cpp b/resources/imap/moveitemstask.cpp
new file mode 100644 (file)
index 0000000..a9d73cc
--- /dev/null
@@ -0,0 +1,319 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "moveitemstask.h"
+
+#include <QtCore/QUuid>
+
+#include "imapresource_debug.h"
+#include <KLocalizedString>
+
+#include <kimap/copyjob.h>
+#include <kimap/searchjob.h>
+#include <kimap/selectjob.h>
+#include <kimap/session.h>
+#include <kimap/storejob.h>
+
+#include <kmime/kmime_message.h>
+
+#include "imapflags.h"
+#include "uidnextattribute.h"
+
+MoveItemsTask::MoveItemsTask(const ResourceStateInterface::Ptr &resource, QObject *parent)
+    : ResourceTask(DeferIfNoSession, resource, parent)
+{
+
+}
+
+MoveItemsTask::~MoveItemsTask()
+{
+}
+
+void MoveItemsTask::doStart(KIMAP::Session *session)
+{
+    if (item().remoteId().isEmpty()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Failed: messages has no rid";
+        emitError(i18n("Cannot move message, it does not exist on the server."));
+        changeProcessed();
+        return;
+    }
+
+    if (sourceCollection().remoteId().isEmpty()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Failed: source collection has no rid";
+        emitError(i18n("Cannot move message out of '%1', '%1' does not exist on the server.",
+                       sourceCollection().name()));
+        changeProcessed();
+        return;
+    }
+
+    if (targetCollection().remoteId().isEmpty()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Failed: target collection has no rid";
+        emitError(i18n("Cannot move message to '%1', '%1' does not exist on the server.",
+                       targetCollection().name()));
+        changeProcessed();
+        return;
+    }
+
+    const QString oldMailBox = mailBoxForCollection(sourceCollection());
+    const QString newMailBox = mailBoxForCollection(targetCollection());
+
+    if (oldMailBox == newMailBox) {
+        qCDebug(IMAPRESOURCE_LOG) << "Nothing to do, same mailbox";
+        changeProcessed();
+        return;
+    }
+
+    if (session->selectedMailBox() != oldMailBox) {
+        KIMAP::SelectJob *select = new KIMAP::SelectJob(session);
+
+        select->setMailBox(oldMailBox);
+        connect(select, &KIMAP::SelectJob::result, this, &MoveItemsTask::onSelectDone);
+
+        select->start();
+    } else {
+        triggerCopyJob(session);
+    }
+}
+
+void MoveItemsTask::onSelectDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Select failed: " << job->errorString();
+        cancelTask(job->errorString());
+
+    } else {
+        KIMAP::SelectJob *select = static_cast<KIMAP::SelectJob *>(job);
+        triggerCopyJob(select->session());
+    }
+}
+
+void MoveItemsTask::triggerCopyJob(KIMAP::Session *session)
+{
+    const QString newMailBox = mailBoxForCollection(targetCollection());
+
+    KIMAP::ImapSet set;
+
+    // save message id, might be needed later to search for the
+    // resulting message uid.
+    foreach (const Akonadi::Item &item, items()) {
+        try {
+            KMime::Message::Ptr msg = item.payload<KMime::Message::Ptr>();
+            const QByteArray messageId = msg->messageID()->asUnicodeString().toUtf8();
+            if (!messageId.isEmpty()) {
+                m_messageIds.insert(item.id(), messageId);
+            }
+
+            set.add(item.remoteId().toLong());
+        } catch (const Akonadi::PayloadException &e) {
+            qCWarning(IMAPRESOURCE_LOG) << "Copy failed, payload exception " << item.id() << item.remoteId();
+            cancelTask(i18n("Failed to copy item, it has no message payload. Remote id: %1", item.remoteId()));
+            return;
+        }
+    }
+
+    KIMAP::CopyJob *copy = new KIMAP::CopyJob(session);
+
+    copy->setUidBased(true);
+    copy->setSequenceSet(set);
+    copy->setMailBox(newMailBox);
+
+    connect(copy, &KIMAP::CopyJob::result, this, &MoveItemsTask::onCopyDone);
+
+    copy->start();
+
+    m_oldSet = set;
+}
+
+void MoveItemsTask::onCopyDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << job->errorString();
+        cancelTask(job->errorString());
+
+    } else {
+        KIMAP::CopyJob *copy = static_cast<KIMAP::CopyJob *>(job);
+
+        m_newUids = imapSetToList(copy->resultingUids());
+
+        // Mark the old one ready for deletion
+        KIMAP::StoreJob *store = new KIMAP::StoreJob(copy->session());
+
+        store->setUidBased(true);
+        store->setSequenceSet(m_oldSet);
+        store->setFlags(QList<QByteArray>() << ImapFlags::Deleted);
+        store->setMode(KIMAP::StoreJob::AppendFlags);
+
+        connect(store, &KIMAP::StoreJob::result, this, &MoveItemsTask::onStoreFlagsDone);
+
+        store->start();
+    }
+}
+
+void MoveItemsTask::onStoreFlagsDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Failed to mark message as deleted on source server: " << job->errorString();
+        emitWarning(i18n("Failed to mark the message from '%1' for deletion on the IMAP server. "
+                         "It will reappear on next sync.",
+                         sourceCollection().name()));
+    }
+
+    if (!m_newUids.isEmpty()) {
+        recordNewUid();
+    } else {
+        // Let's go for a search to find the new UID :-)
+
+        // We did a copy we're very likely not in the right mailbox
+        KIMAP::StoreJob *store = static_cast<KIMAP::StoreJob *>(job);
+        KIMAP::SelectJob *select = new KIMAP::SelectJob(store->session());
+        select->setMailBox(mailBoxForCollection(targetCollection()));
+
+        connect(select, &KIMAP::SelectJob::result, this, &MoveItemsTask::onPreSearchSelectDone);
+
+        select->start();
+    }
+}
+
+void MoveItemsTask::onPreSearchSelectDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Select failed: " << job->errorString();
+        cancelTask(job->errorString());
+        return;
+    }
+
+    KIMAP::SelectJob *select = static_cast<KIMAP::SelectJob *>(job);
+    KIMAP::SearchJob *search = new KIMAP::SearchJob(select->session());
+
+    search->setUidBased(true);
+
+    if (!m_messageIds.isEmpty()) {
+        search->setSearchLogic(KIMAP::SearchJob::Or);
+
+        foreach (const QByteArray &messageId, m_messageIds) {
+            QByteArray header = "Message-ID ";
+            header += messageId;
+            search->addSearchCriteria(KIMAP::SearchJob::Header, header);
+        }
+    } else {
+        search->setSearchLogic(KIMAP::SearchJob::And);
+        search->addSearchCriteria(KIMAP::SearchJob::New);
+
+        Akonadi::Collection c = targetCollection();
+        UidNextAttribute *uidNext = c.attribute<UidNextAttribute>();
+        if (!uidNext) {
+            cancelTask(i18n("Could not determine the UID for the newly created message on the server"));
+            search->deleteLater();
+            return;
+        }
+        KIMAP::ImapInterval interval(uidNext->uidNext());
+
+        search->addSearchCriteria(KIMAP::SearchJob::Uid, interval.toImapSequence());
+    }
+
+    connect(search, &KIMAP::SearchJob::result, this, &MoveItemsTask::onSearchDone);
+
+    search->start();
+}
+
+void MoveItemsTask::onSearchDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Search failed: " << job->errorString();
+        cancelTask(job->errorString());
+        return;
+    }
+
+    KIMAP::SearchJob *search = static_cast<KIMAP::SearchJob *>(job);
+    m_newUids = search->results();
+
+    recordNewUid();
+}
+
+void MoveItemsTask::recordNewUid()
+{
+    // Create the item resulting of the operation, since at that point
+    // the first part of the move succeeded
+    QVector<qint64> oldUids = imapSetToList(m_oldSet);
+
+    Akonadi::Item::List newItems;
+    for (int i = 0; i < oldUids.count(); ++i) {
+        const QString oldUid = QString::number(oldUids.at(i));
+        Akonadi::Item item;
+        Q_FOREACH (const Akonadi::Item &it, items()) {
+            if (it.remoteId() == oldUid) {
+                item = it;
+                break;
+            }
+        }
+        Q_ASSERT(item.isValid());
+
+        // Update the item content with the new UID from the copy
+        // if we didn't manage to get a valid UID from the server, use a random RID instead
+        // this will make ItemSync clean up the mess during the next sync (while empty RIDs are protected as not yet existing on the server)
+        if (m_newUids.count() <= i) {
+            item.setRemoteId(QUuid::createUuid().toString());
+        } else {
+            item.setRemoteId(QString::number(m_newUids.at(i)));
+        }
+        newItems << item;
+    }
+
+    changesCommitted(newItems);
+
+    Akonadi::Collection c = targetCollection();
+
+    // Get the current uid next value and store it
+    UidNextAttribute *uidAttr = Q_NULLPTR;
+    int oldNextUid = 0;
+    if (c.hasAttribute("uidnext")) {
+        uidAttr = static_cast<UidNextAttribute *>(c.attribute("uidnext"));
+        oldNextUid = uidAttr->uidNext();
+    }
+
+    // If the uid we just got back is the expected next one of the box
+    // then update the property to the probable next uid to keep the cache in sync.
+    // If not something happened in our back, so we don't update and a refetch will
+    // happen at some point.
+    if (!m_newUids.isEmpty() && m_newUids.last() == oldNextUid) {
+        if (uidAttr == Q_NULLPTR) {
+            uidAttr = new UidNextAttribute(m_newUids.last() + 1);
+            c.addAttribute(uidAttr);
+        } else {
+            uidAttr->setUidNext(m_newUids.last() + 1);
+        }
+
+        applyCollectionChanges(c);
+    }
+}
+
+QVector<qint64> MoveItemsTask::imapSetToList(const KIMAP::ImapSet &set)
+{
+    QVector<qint64> list;
+    foreach (const KIMAP::ImapInterval &interval, set.intervals()) {
+        for (qint64 i = interval.begin(); i <= interval.end(); ++i) {
+            list << i;
+        }
+    }
+
+    return list;
+}
+
diff --git a/resources/imap/moveitemstask.h b/resources/imap/moveitemstask.h
new file mode 100644 (file)
index 0000000..5368040
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef MOVEITEMSTASK_H
+#define MOVEITEMSTASK_H
+
+#include "resourcetask.h"
+
+#include <kimap/imapset.h>
+
+class MoveItemsTask : public ResourceTask
+{
+    Q_OBJECT
+
+public:
+    explicit MoveItemsTask(const ResourceStateInterface::Ptr &resource, QObject *parent = Q_NULLPTR);
+    virtual ~MoveItemsTask();
+
+private Q_SLOTS:
+    void onSelectDone(KJob *job);
+    void onCopyDone(KJob *job);
+    void onStoreFlagsDone(KJob *job);
+
+    void onPreSearchSelectDone(KJob *job);
+    void onSearchDone(KJob *job);
+
+protected:
+    void doStart(KIMAP::Session *session) Q_DECL_OVERRIDE;
+
+private:
+    void triggerCopyJob(KIMAP::Session *session);
+    void recordNewUid();
+    QVector<qint64> imapSetToList(const KIMAP::ImapSet &set);
+
+    KIMAP::ImapSet m_oldSet;
+    QVector<qint64> m_newUids;
+    QMap<Akonadi::Item::Id /* original ID */, QByteArray> m_messageIds;
+};
+
+#endif
diff --git a/resources/imap/noinferiorsattribute.cpp b/resources/imap/noinferiorsattribute.cpp
new file mode 100644 (file)
index 0000000..0fbb778
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+    Copyright (C) 2012  Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#include "noinferiorsattribute.h"
+
+#include <QByteArray>
+
+#include <attribute.h>
+
+NoInferiorsAttribute::NoInferiorsAttribute(bool noInferiors)
+    : mNoInferiors(noInferiors)
+{
+}
+
+void NoInferiorsAttribute::setNoInferiors(bool noInferiors)
+{
+    mNoInferiors = noInferiors;
+}
+
+bool NoInferiorsAttribute::noInferiors() const
+{
+    return mNoInferiors;
+}
+
+QByteArray NoInferiorsAttribute::type() const
+{
+    static const QByteArray sType("noinferiors");
+    return sType;
+}
+
+Akonadi::Attribute *NoInferiorsAttribute::clone() const
+{
+    return new NoInferiorsAttribute(mNoInferiors);
+}
+
+QByteArray NoInferiorsAttribute::serialized() const
+{
+    return mNoInferiors ? QByteArray::number(1) :  QByteArray::number(0);
+}
+
+void NoInferiorsAttribute::deserialize(const QByteArray &data)
+{
+    mNoInferiors = (data.toInt() == 0) ? false : true;
+}
diff --git a/resources/imap/noinferiorsattribute.h b/resources/imap/noinferiorsattribute.h
new file mode 100644 (file)
index 0000000..6113bd2
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+    Copyright (C) 2012  Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#ifndef NOINFERIORSATTRIBUTE_H
+#define NOINFERIORSATTRIBUTE_H
+
+#include <attribute.h>
+
+class NoInferiorsAttribute : public Akonadi::Attribute
+{
+public:
+    explicit NoInferiorsAttribute(bool noInferiors = false);
+    void setNoInferiors(bool noInferiors);
+    bool noInferiors() const;
+    QByteArray type() const Q_DECL_OVERRIDE;
+    Attribute *clone() const Q_DECL_OVERRIDE;
+    QByteArray serialized() const Q_DECL_OVERRIDE;
+    void deserialize(const QByteArray &data) Q_DECL_OVERRIDE;
+
+private:
+    bool mNoInferiors;
+};
+
+#endif // NOINFERIORSATTRIBUTE_H
diff --git a/resources/imap/noselectattribute.cpp b/resources/imap/noselectattribute.cpp
new file mode 100644 (file)
index 0000000..f2c0c07
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+    Copyright (C) 2008 Omat Holding B.V. <info@omat.nl>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "noselectattribute.h"
+
+#include <QByteArray>
+
+#include <attribute.h>
+
+NoSelectAttribute::NoSelectAttribute(bool noSelect)
+    : mNoSelect(noSelect)
+{
+}
+
+void NoSelectAttribute::setNoSelect(bool noSelect)
+{
+    mNoSelect = noSelect;
+}
+
+bool NoSelectAttribute::noSelect() const
+{
+    return mNoSelect;
+}
+
+QByteArray NoSelectAttribute::type() const
+{
+    static const QByteArray sType("noselect");
+    return sType;
+}
+
+Akonadi::Attribute *NoSelectAttribute::clone() const
+{
+    return new NoSelectAttribute(mNoSelect);
+}
+
+QByteArray NoSelectAttribute::serialized() const
+{
+    return mNoSelect ? QByteArray::number(1) :  QByteArray::number(0);
+}
+
+void NoSelectAttribute::deserialize(const QByteArray &data)
+{
+    mNoSelect = (data.toInt() == 0) ? false : true;
+}
diff --git a/resources/imap/noselectattribute.h b/resources/imap/noselectattribute.h
new file mode 100644 (file)
index 0000000..4f33a68
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+    Copyright (C) 2008 Omat Holding B.V. <info@omat.nl>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef NOSELECTATTRIBUTE_H
+#define NOSELECTATTRIBUTE_H
+
+#include <attribute.h>
+
+class NoSelectAttribute : public Akonadi::Attribute
+{
+public:
+    explicit NoSelectAttribute(bool noSelect = false);
+    void setNoSelect(bool noSelect);
+    bool noSelect() const;
+    QByteArray type() const Q_DECL_OVERRIDE;
+    Attribute *clone() const Q_DECL_OVERRIDE;
+    QByteArray serialized() const Q_DECL_OVERRIDE;
+    void deserialize(const QByteArray &data) Q_DECL_OVERRIDE;
+
+private:
+    bool mNoSelect;
+};
+
+#endif
diff --git a/resources/imap/passwordrequesterinterface.cpp b/resources/imap/passwordrequesterinterface.cpp
new file mode 100644 (file)
index 0000000..029d029
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                         a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "passwordrequesterinterface.h"
+
+PasswordRequesterInterface::PasswordRequesterInterface(QObject *parent)
+    : QObject(parent)
+{
+
+}
+
+void PasswordRequesterInterface::cancelPasswordRequests()
+{
+
+}
diff --git a/resources/imap/passwordrequesterinterface.h b/resources/imap/passwordrequesterinterface.h
new file mode 100644 (file)
index 0000000..413acf6
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef PASSWORDREQUESTERINTERFACE_H
+#define PASSWORDREQUESTERINTERFACE_H
+
+#include <QtCore/QObject>
+
+class PasswordRequesterInterface : public QObject
+{
+    Q_OBJECT
+    Q_ENUMS(ResultType RequestType)
+
+public:
+    enum ResultType {
+        PasswordRetrieved,
+        ReconnectNeeded,
+        UserRejected,
+        EmptyPasswordEntered
+    };
+
+    enum RequestType {
+        StandardRequest,
+        WrongPasswordRequest
+    };
+
+protected:
+    explicit PasswordRequesterInterface(QObject *parent = Q_NULLPTR);
+
+public:
+    virtual void requestPassword(RequestType request = StandardRequest,
+                                 const QString &serverError = QString()) = 0;
+    virtual void cancelPasswordRequests();
+
+Q_SIGNALS:
+    void done(int resultType, const QString &password = QString());
+};
+
+#endif
diff --git a/resources/imap/removecollectionrecursivetask.cpp b/resources/imap/removecollectionrecursivetask.cpp
new file mode 100644 (file)
index 0000000..7bae4c9
--- /dev/null
@@ -0,0 +1,171 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Tobias Koenig <tokoe@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "removecollectionrecursivetask.h"
+
+#include <Akonadi/KMime/MessageFlags>
+#include <kimap/deletejob.h>
+#include <kimap/expungejob.h>
+#include <kimap/selectjob.h>
+#include <kimap/storejob.h>
+#include <kimap/closejob.h>
+#include <KLocalizedString>
+#include "imapresource_debug.h"
+#include "imapresource_debug.h"
+
+Q_DECLARE_METATYPE(KIMAP::DeleteJob *)
+
+RemoveCollectionRecursiveTask::RemoveCollectionRecursiveTask(ResourceStateInterface::Ptr resource, QObject *parent)
+    : ResourceTask(CancelIfNoSession, resource, parent),
+      mSession(Q_NULLPTR), mFolderFound(false)
+{
+}
+
+RemoveCollectionRecursiveTask::~RemoveCollectionRecursiveTask()
+{
+}
+
+void RemoveCollectionRecursiveTask::doStart(KIMAP::Session *session)
+{
+    mSession = session;
+
+    mFolderFound = false;
+    KIMAP::ListJob *listJob = new KIMAP::ListJob(session);
+    listJob->setIncludeUnsubscribed(!isSubscriptionEnabled());
+    listJob->setQueriedNamespaces(serverNamespaces());
+    connect(listJob, &KIMAP::ListJob::mailBoxesReceived,
+            this, &RemoveCollectionRecursiveTask::onMailBoxesReceived);
+    connect(listJob, &KIMAP::ListJob::result, this, &RemoveCollectionRecursiveTask::onJobDone);
+    listJob->start();
+}
+
+void RemoveCollectionRecursiveTask::onMailBoxesReceived(const QList< KIMAP::MailBoxDescriptor > &descriptors,
+        const QList< QList<QByteArray> > &)
+{
+    const QString mailBox = mailBoxForCollection(collection());
+
+    // We have to delete the deepest-nested folders first, so
+    // we use a map here that has the level of nesting as key.
+    QMultiMap<int, KIMAP::MailBoxDescriptor > foldersToDelete;
+
+    for (int i = 0; i < descriptors.size(); ++i) {
+        const KIMAP::MailBoxDescriptor descriptor = descriptors[ i ];
+
+        if (descriptor.name == mailBox || descriptor.name.startsWith(mailBox + descriptor.separator)) {     // a sub folder to delete
+            const QStringList pathParts = descriptor.name.split(descriptor.separator);
+            foldersToDelete.insert(pathParts.count(), descriptor);
+        }
+    }
+
+    if (foldersToDelete.isEmpty()) {
+        return;
+    }
+
+    mFolderFound = true;
+
+    // Now start the actual deletion work
+    mFolderIterator.reset(new QMapIterator<int, KIMAP::MailBoxDescriptor >(foldersToDelete));
+    mFolderIterator->toBack(); // we start with largest nesting value first
+
+    deleteNextMailbox();
+}
+
+void RemoveCollectionRecursiveTask::deleteNextMailbox()
+{
+    if (!mFolderIterator->hasPrevious()) {
+        changeProcessed(); // finish the job
+        return;
+    }
+
+    mFolderIterator->previous();
+    const KIMAP::MailBoxDescriptor &descriptor = mFolderIterator->value();
+    qCDebug(IMAPRESOURCE_LOG) << descriptor.name;
+
+    // first select the mailbox
+    KIMAP::SelectJob *selectJob = new KIMAP::SelectJob(mSession);
+    selectJob->setMailBox(descriptor.name);
+    connect(selectJob, &KIMAP::SelectJob::result, this, &RemoveCollectionRecursiveTask::onJobDone);
+    selectJob->start();
+
+    // mark all items as deleted
+    // This step shouldn't be required, but apparently some servers don't allow deleting, non empty mailboxes (although they should).
+    KIMAP::ImapSet allItems;
+    allItems.add(KIMAP::ImapInterval(1, 0));     // means 1:*
+    KIMAP::StoreJob *storeJob = new KIMAP::StoreJob(mSession);
+    storeJob->setSequenceSet(allItems);
+    storeJob->setFlags(KIMAP::MessageFlags() << Akonadi::MessageFlags::Deleted);
+    storeJob->setMode(KIMAP::StoreJob::AppendFlags);
+    // The result is explicitly ignored, since this can fail in the case of an empty folder
+    storeJob->start();
+
+    // Some IMAP servers don't allow deleting an opened mailbox, so make sure
+    // it's not opened (https://bugs.kde.org/show_bug.cgi?id=324932). CLOSE will
+    // also trigger EXPUNGE to take care of the messages deleted above
+    KIMAP::CloseJob *closeJob = new KIMAP::CloseJob(mSession);
+    closeJob->setProperty("folderDescriptor", descriptor.name);
+    connect(closeJob, &KIMAP::CloseJob::result, this, &RemoveCollectionRecursiveTask::onCloseJobDone);
+    closeJob->start();
+}
+
+void RemoveCollectionRecursiveTask::onCloseJobDone(KJob *job)
+{
+    if (job->error()) {
+        changeProcessed();
+        qCDebug(IMAPRESOURCE_LOG) << "Failed to close the folder, resync the folder tree";
+        emitWarning(i18n("Failed to delete the folder, restoring folder list."));
+        synchronizeCollectionTree();
+    } else {
+        KIMAP::DeleteJob *deleteJob = new KIMAP::DeleteJob(mSession);
+        deleteJob->setMailBox(job->property("folderDescriptor").toString());
+        connect(deleteJob, &KIMAP::DeleteJob::result, this, &RemoveCollectionRecursiveTask::onDeleteJobDone);
+        deleteJob->start();
+    }
+}
+
+void RemoveCollectionRecursiveTask::onDeleteJobDone(KJob *job)
+{
+    if (job->error()) {
+        changeProcessed();
+
+        qCDebug(IMAPRESOURCE_LOG) << "Failed to delete the folder, resync the folder tree";
+        emitWarning(i18n("Failed to delete the folder, restoring folder list."));
+        synchronizeCollectionTree();
+    } else {
+        deleteNextMailbox();
+    }
+}
+
+void RemoveCollectionRecursiveTask::onJobDone(KJob *job)
+{
+    if (job->error()) {
+        changeProcessed();
+
+        qCDebug(IMAPRESOURCE_LOG) << "Failed to delete the folder, resync the folder tree";
+        emitWarning(i18n("Failed to delete the folder, restoring folder list."));
+        synchronizeCollectionTree();
+    } else if (!mFolderFound) {
+        changeProcessed();
+        qCDebug(IMAPRESOURCE_LOG) << "Failed to find the folder to be deleted, resync the folder tree";
+        emitWarning(i18n("Failed to find the folder to be deleted, restoring folder list."));
+        synchronizeCollectionTree();
+    }
+}
+
diff --git a/resources/imap/removecollectionrecursivetask.h b/resources/imap/removecollectionrecursivetask.h
new file mode 100644 (file)
index 0000000..5c519eb
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Tobias Koenig <tokoe@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef REMOVECOLLECTIONRECURSIVETASK_H
+#define REMOVECOLLECTIONRECURSIVETASK_H
+
+#include <kimap/listjob.h>
+
+#include "resourcetask.h"
+
+class RemoveCollectionRecursiveTask : public ResourceTask
+{
+    Q_OBJECT
+
+public:
+    explicit RemoveCollectionRecursiveTask(ResourceStateInterface::Ptr resource, QObject *parent = Q_NULLPTR);
+    virtual ~RemoveCollectionRecursiveTask();
+
+private Q_SLOTS:
+    void onMailBoxesReceived(const QList<KIMAP::MailBoxDescriptor> &descriptors,
+                             const QList< QList<QByteArray> > &flags);
+    void onCloseJobDone(KJob *job);
+    void onDeleteJobDone(KJob *job);
+    void onJobDone(KJob *job);
+
+protected:
+    void doStart(KIMAP::Session *session) Q_DECL_OVERRIDE;
+
+private:
+    void deleteNextMailbox();
+
+    KIMAP::Session *mSession;
+    bool mFolderFound;
+
+    QScopedPointer< QMapIterator<int, KIMAP::MailBoxDescriptor > > mFolderIterator;
+};
+
+#endif
diff --git a/resources/imap/replacemessagejob.cpp b/resources/imap/replacemessagejob.cpp
new file mode 100644 (file)
index 0000000..1bc28a2
--- /dev/null
@@ -0,0 +1,184 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "replacemessagejob.h"
+
+#include <KIMAP/AppendJob>
+#include <KIMAP/SearchJob>
+#include <KIMAP/SelectJob>
+#include <KIMAP/StoreJob>
+#include <KIMAP/ImapSet>
+#include "imapresource_debug.h"
+#include <KMime/Message>
+
+#include "imapflags.h"
+
+ReplaceMessageJob::ReplaceMessageJob(const KMime::Message::Ptr &msg, KIMAP::Session *session, const QString &mailbox, qint64 uidNext, KIMAP::ImapSet oldUids, QObject *parent)
+    : KJob(parent),
+      mSession(session),
+      mMessage(msg),
+      mMailbox(mailbox),
+      mUidNext(uidNext),
+      mOldUids(oldUids),
+      mNewUid(-1),
+      mMessageId(msg->messageID()->asUnicodeString().toUtf8())
+{
+}
+
+void ReplaceMessageJob::start()
+{
+    KIMAP::AppendJob *job = new KIMAP::AppendJob(mSession);
+    job->setMailBox(mMailbox);
+    job->setContent(mMessage->encodedContent(true));
+    job->setInternalDate(mMessage->date()->dateTime());
+    connect(job, &KJob::result, this, &ReplaceMessageJob::onAppendMessageDone);
+    job->start();
+}
+
+void ReplaceMessageJob::onAppendMessageDone(KJob *job)
+{
+    KIMAP::AppendJob *append = qobject_cast<KIMAP::AppendJob *>(job);
+
+    if (append->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << append->errorString();
+        setError(KJob::UserDefinedError);
+        emitResult();
+        return;
+    }
+
+    // We get it directly if UIDPLUS is supported...
+    mNewUid = append->uid();
+
+    if (mNewUid > 0 && mOldUids.isEmpty()) {
+        //We have the uid an no message to delete, we're done
+        emitResult();
+        return;
+    }
+
+    if (mSession->selectedMailBox() != mMailbox) {
+        //For search and delete we need to select the right mailbox first
+        KIMAP::SelectJob *select = new KIMAP::SelectJob(mSession);
+        select->setMailBox(mMailbox);
+        connect(select, &KJob::result, this, &ReplaceMessageJob::onSelectDone);
+        select->start();
+    } else {
+        if (mNewUid > 0) {
+            triggerDeleteJobIfNecessary();
+        } else {
+            triggerSearchJob();
+        }
+    }
+}
+
+void ReplaceMessageJob::onSelectDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << job->errorString();
+        setError(KJob::UserDefinedError);
+        emitResult();
+    } else {
+        if (mNewUid > 0) {
+            triggerDeleteJobIfNecessary();
+        } else {
+            triggerSearchJob();
+        }
+    }
+}
+
+void ReplaceMessageJob::triggerSearchJob()
+{
+    KIMAP::SearchJob *search = new KIMAP::SearchJob(mSession);
+
+    search->setUidBased(true);
+    search->setSearchLogic(KIMAP::SearchJob::And);
+
+    if (!mMessageId.isEmpty()) {
+        QByteArray header = "Message-ID ";
+        header += mMessageId;
+
+        search->addSearchCriteria(KIMAP::SearchJob::Header, header);
+    } else {
+        search->addSearchCriteria(KIMAP::SearchJob::New);
+
+        if (mUidNext < 0) {
+            qCWarning(IMAPRESOURCE_LOG) << "Could not determine the UID for the newly created message on the server";
+            search->deleteLater();
+            setError(KJob::UserDefinedError);
+            emitResult();
+            return;
+        }
+        search->addSearchCriteria(KIMAP::SearchJob::Uid, KIMAP::ImapInterval(mUidNext).toImapSequence());
+    }
+
+    connect(search, &KJob::result,
+            this, &ReplaceMessageJob::onSearchDone);
+
+    search->start();
+}
+
+void ReplaceMessageJob::onSearchDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << job->errorString();
+        setError(KJob::UserDefinedError);
+        emitResult();
+        return;
+    }
+
+    KIMAP::SearchJob *search = static_cast<KIMAP::SearchJob *>(job);
+
+    if (search->results().count() == 1) {
+        mNewUid = search->results().at(0);
+    } else {
+        qCWarning(IMAPRESOURCE_LOG) << "Failed to find uid for message. Got 0 or too many results: " << search->results().count();
+        setError(KJob::UserDefinedError);
+        emitResult();
+        return;
+    }
+    triggerDeleteJobIfNecessary();
+}
+
+void ReplaceMessageJob::triggerDeleteJobIfNecessary()
+{
+    if (mOldUids.isEmpty()) {
+        //Nothing to do, we're done
+        emitResult();
+    } else {
+        KIMAP::StoreJob *store = new KIMAP::StoreJob(mSession);
+        store->setUidBased(true);
+        store->setSequenceSet(mOldUids);
+        store->setFlags(QList<QByteArray>() << ImapFlags::Deleted);
+        store->setMode(KIMAP::StoreJob::AppendFlags);
+        connect(store, &KJob::result, this, &ReplaceMessageJob::onDeleteDone);
+        store->start();
+    }
+}
+
+void ReplaceMessageJob::onDeleteDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << job->errorString();
+    }
+    emitResult();
+}
+
+qint64 ReplaceMessageJob::newUid() const
+{
+    return mNewUid;
+}
diff --git a/resources/imap/replacemessagejob.h b/resources/imap/replacemessagejob.h
new file mode 100644 (file)
index 0000000..5818042
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef REPLACEMESSAGEJOB_H
+#define REPLACEMESSAGEJOB_H
+
+#include <KJob>
+#include <KMime/Message>
+#include <KIMAP/Session>
+#include <KIMAP/ImapSet>
+
+/**
+ * This job appends a message, marks the old one as deleted, and returns the uid of the appended message.
+ */
+class ReplaceMessageJob : public KJob
+{
+    Q_OBJECT
+public:
+    ReplaceMessageJob(const KMime::Message::Ptr &msg, KIMAP::Session *session, const QString &mailbox, qint64 uidNext = -1, KIMAP::ImapSet oldUids = KIMAP::ImapSet(), QObject *parent = Q_NULLPTR);
+
+    qint64 newUid() const;
+
+    void start() Q_DECL_OVERRIDE;
+
+private:
+    void triggerSearchJob();
+    void triggerDeleteJobIfNecessary();
+
+private Q_SLOTS:
+    void onAppendMessageDone(KJob *job);
+    void onSelectDone(KJob *job);
+    void onSearchDone(KJob *job);
+    void onDeleteDone(KJob *job);
+
+private:
+    KIMAP::Session *mSession;
+    const KMime::Message::Ptr mMessage;
+    const QString mMailbox;
+    qint64 mUidNext;
+    KIMAP::ImapSet mOldUids;
+    qint64 mNewUid;
+    const QByteArray mMessageId;
+};
+
+#endif
+
diff --git a/resources/imap/resourcestate.cpp b/resources/imap/resourcestate.cpp
new file mode 100644 (file)
index 0000000..bbd78bc
--- /dev/null
@@ -0,0 +1,369 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "resourcestate.h"
+
+#include "imapaccount.h"
+#include "imapresource.h"
+#include "sessionpool.h"
+#include "settings.h"
+#include "noselectattribute.h"
+
+#include <collectionmodifyjob.h>
+#include <agentsearchinterface.h>
+#include <kmessagebox.h>
+#include "imapresource_debug.h"
+
+ResourceState::ResourceState(ImapResourceBase *resource, const TaskArguments &args)
+    : m_resource(resource),
+      m_arguments(args)
+{
+
+}
+
+ResourceState::~ResourceState()
+{
+
+}
+
+QString ResourceState::userName() const
+{
+    return m_resource->m_pool->account()->userName();
+}
+
+QString ResourceState::resourceName() const
+{
+    return m_resource->name();
+}
+
+QString ResourceState::resourceIdentifier() const
+{
+    return m_resource->identifier();
+}
+
+QStringList ResourceState::serverCapabilities() const
+{
+    return m_resource->m_pool->serverCapabilities();
+}
+
+QList<KIMAP::MailBoxDescriptor> ResourceState::serverNamespaces() const
+{
+    return m_resource->m_pool->serverNamespaces();
+}
+
+QList<KIMAP::MailBoxDescriptor> ResourceState::personalNamespaces() const
+{
+    return m_resource->m_pool->serverNamespaces(SessionPool::Personal);
+}
+
+QList<KIMAP::MailBoxDescriptor> ResourceState::userNamespaces() const
+{
+    return m_resource->m_pool->serverNamespaces(SessionPool::User);
+}
+
+QList<KIMAP::MailBoxDescriptor> ResourceState::sharedNamespaces() const
+{
+    return m_resource->m_pool->serverNamespaces(SessionPool::Shared);
+}
+
+bool ResourceState::isAutomaticExpungeEnabled() const
+{
+    return m_resource->settings()->automaticExpungeEnabled();
+}
+
+bool ResourceState::isSubscriptionEnabled() const
+{
+    return m_resource->settings()->subscriptionEnabled();
+}
+
+bool ResourceState::isDisconnectedModeEnabled() const
+{
+    return m_resource->settings()->disconnectedModeEnabled();
+}
+
+int ResourceState::intervalCheckTime() const
+{
+    if (m_resource->settings()->intervalCheckEnabled()) {
+        return m_resource->settings()->intervalCheckTime();
+    } else {
+        return -1;    // -1 for never
+    }
+}
+
+Akonadi::Collection ResourceState::collection() const
+{
+    return m_arguments.collection;
+}
+
+Akonadi::Item ResourceState::item() const
+{
+    if (m_arguments.items.count() > 1) {
+        qCWarning(IMAPRESOURCE_LOG) << "Called item() while state holds multiple items!";
+    }
+
+    return m_arguments.items.first();
+}
+
+Akonadi::Item::List ResourceState::items() const
+{
+    return m_arguments.items;
+}
+
+Akonadi::Collection ResourceState::parentCollection() const
+{
+    return m_arguments.parentCollection;
+}
+
+Akonadi::Collection ResourceState::sourceCollection() const
+{
+    return m_arguments.sourceCollection;
+}
+
+Akonadi::Collection ResourceState::targetCollection() const
+{
+    return m_arguments.targetCollection;
+}
+
+QSet<QByteArray> ResourceState::parts() const
+{
+    return m_arguments.parts;
+}
+
+QSet<QByteArray> ResourceState::addedFlags() const
+{
+    return m_arguments.addedFlags;
+}
+
+QSet<QByteArray> ResourceState::removedFlags() const
+{
+    return m_arguments.removedFlags;
+}
+
+Akonadi::Tag ResourceState::tag() const
+{
+    return m_arguments.tag;
+}
+
+QSet<Akonadi::Tag> ResourceState::addedTags() const
+{
+    return m_arguments.addedTags;
+}
+
+QSet<Akonadi::Tag> ResourceState::removedTags() const
+{
+    return m_arguments.removedTags;
+}
+
+Akonadi::Relation::List ResourceState::addedRelations() const
+{
+    return m_arguments.addedRelations;
+}
+
+Akonadi::Relation::List ResourceState::removedRelations() const
+{
+    return m_arguments.removedRelations;
+}
+
+QString ResourceState::rootRemoteId() const
+{
+    return m_resource->settings()->rootRemoteId();
+}
+
+void ResourceState::setIdleCollection(const Akonadi::Collection &collection)
+{
+    QStringList ridPath;
+
+    Akonadi::Collection curCol = collection;
+    while (curCol != Akonadi::Collection::root() && !curCol.remoteId().isEmpty()) {
+        ridPath.append(curCol.remoteId());
+        curCol = curCol.parentCollection();
+    }
+
+    m_resource->settings()->setIdleRidPath(ridPath);
+    m_resource->settings()->save();
+}
+
+void ResourceState::applyCollectionChanges(const Akonadi::Collection &collection)
+{
+    m_resource->modifyCollection(collection);
+}
+
+void ResourceState::collectionAttributesRetrieved(const Akonadi::Collection &collection)
+{
+    m_resource->collectionAttributesRetrieved(collection);
+}
+
+void ResourceState::itemRetrieved(const Akonadi::Item &item)
+{
+    m_resource->itemRetrieved(item);
+}
+
+void ResourceState::itemsRetrieved(const Akonadi::Item::List &items)
+{
+    m_resource->itemsRetrieved(items);
+}
+
+void ResourceState::itemsRetrievedIncremental(const Akonadi::Item::List &changed, const Akonadi::Item::List &removed)
+{
+    m_resource->itemsRetrievedIncremental(changed, removed);
+}
+
+void ResourceState::itemsRetrievalDone()
+{
+    m_resource->itemsRetrievalDone();
+    emitPercent(100);
+}
+
+void ResourceState::setTotalItems(int items)
+{
+    m_resource->setTotalItems(items);
+}
+
+void ResourceState::itemChangeCommitted(const Akonadi::Item &item)
+{
+    m_resource->changeCommitted(item);
+}
+
+void ResourceState::itemsChangesCommitted(const Akonadi::Item::List &items)
+{
+    m_resource->changesCommitted(items);
+}
+
+void ResourceState::collectionsRetrieved(const Akonadi::Collection::List &collections)
+{
+    m_resource->collectionsRetrieved(collections);
+    m_resource->startIdleIfNeeded();
+}
+
+void ResourceState::collectionChangeCommitted(const Akonadi::Collection &collection)
+{
+    m_resource->changeCommitted(collection);
+}
+
+void ResourceState::tagChangeCommitted(const Akonadi::Tag &tag)
+{
+    m_resource->changeCommitted(tag);
+}
+
+void ResourceState::changeProcessed()
+{
+    m_resource->changeProcessed();
+}
+
+void ResourceState::searchFinished(const QVector<qint64> &result, bool isRid)
+{
+    m_resource->searchFinished(result, isRid ? Akonadi::AgentSearchInterface::Rid :
+                               Akonadi::AgentSearchInterface::Uid);
+}
+
+void ResourceState::cancelTask(const QString &errorString)
+{
+    m_resource->cancelTask(errorString);
+}
+
+void ResourceState::deferTask()
+{
+    m_resource->deferTask();
+}
+
+void ResourceState::restartItemRetrieval(Akonadi::Collection::Id col)
+{
+    //This ensures the collection fetch job is rerun (it isn't when using deferTask)
+    //The task will be appended
+    //TODO: deferTask should rerun the collectionfetchjob
+    m_resource->synchronizeCollection(col);
+    cancelTask(QStringLiteral("Restarting item retrieval."));
+}
+
+void ResourceState::taskDone()
+{
+    m_resource->taskDone();
+}
+
+void ResourceState::emitError(const QString &message)
+{
+    Q_EMIT m_resource->error(message);
+}
+
+void ResourceState::emitWarning(const QString &message)
+{
+    Q_EMIT m_resource->warning(message);
+}
+
+void ResourceState::emitPercent(int percent)
+{
+    Q_EMIT m_resource->percent(percent);
+}
+
+void ResourceState::synchronizeCollection(Akonadi::Collection::Id id)
+{
+    m_resource->synchronizeCollection(id);
+}
+
+void ResourceState::synchronizeCollectionTree()
+{
+    m_resource->synchronizeCollectionTree();
+}
+
+void ResourceState::scheduleConnectionAttempt()
+{
+    m_resource->scheduleConnectionAttempt();
+}
+
+QChar ResourceState::separatorCharacter() const
+{
+    return m_resource->separatorCharacter();
+}
+
+void ResourceState::setSeparatorCharacter(const QChar &separator)
+{
+    m_resource->setSeparatorCharacter(separator);
+}
+
+void ResourceState::showInformationDialog(const QString &message, const QString &title, const QString &dontShowAgainName)
+{
+    KMessageBox::information(Q_NULLPTR, message, title, dontShowAgainName);
+}
+
+int ResourceState::batchSize() const
+{
+    return m_resource->itemSyncBatchSize();
+}
+
+MessageHelper::Ptr ResourceState::messageHelper() const
+{
+    return MessageHelper::Ptr(new MessageHelper());
+}
+
+void ResourceState::tagsRetrieved(const Akonadi::Tag::List &tags, const QHash<QString, Akonadi::Item::List> &tagMembers)
+{
+    m_resource->tagsRetrieved(tags, tagMembers);
+}
+
+void ResourceState::relationsRetrieved(const Akonadi::Relation::List &relations)
+{
+    m_resource->relationsRetrieved(relations);
+}
+
+void ResourceState::setItemMergingMode(Akonadi::ItemSync::MergeMode mode)
+{
+    m_resource->setItemMergingMode(mode);
+}
diff --git a/resources/imap/resourcestate.h b/resources/imap/resourcestate.h
new file mode 100644 (file)
index 0000000..650f293
--- /dev/null
@@ -0,0 +1,163 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef RESOURCESTATE_H
+#define RESOURCESTATE_H
+
+#include "resourcestateinterface.h"
+#include "messagehelper.h"
+
+class ImapResourceBase;
+
+struct TaskArguments {
+    TaskArguments() {}
+    TaskArguments(const Akonadi::Item &_item): items(Akonadi::Item::List() << _item) {}
+    TaskArguments(const Akonadi::Item &_item, const Akonadi::Collection &_collection): collection(_collection), items(Akonadi::Item::List() << _item) {}
+    TaskArguments(const Akonadi::Item &_item, const QSet<QByteArray> &_parts): items(Akonadi::Item::List() << _item), parts(_parts) {}
+    TaskArguments(const Akonadi::Item::List &_items): items(_items) {}
+    TaskArguments(const Akonadi::Item::List &_items, const QSet<QByteArray> &_addedFlags, const QSet<QByteArray> &_removedFlags): items(_items), addedFlags(_addedFlags), removedFlags(_removedFlags) {}
+    TaskArguments(const Akonadi::Item::List &_items, const Akonadi::Collection &_sourceCollection, const Akonadi::Collection &_targetCollection): items(_items), sourceCollection(_sourceCollection), targetCollection(_targetCollection) {}
+    TaskArguments(const Akonadi::Item::List &_items, const QSet<Akonadi::Tag> &_addedTags, const QSet<Akonadi::Tag> &_removedTags): items(_items), addedTags(_addedTags), removedTags(_removedTags) {}
+    TaskArguments(const Akonadi::Item::List &_items, const Akonadi::Relation::List &_addedRelations, const Akonadi::Relation::List &_removedRelations): items(_items), addedRelations(_addedRelations), removedRelations(_removedRelations) {}
+    TaskArguments(const Akonadi::Collection &_collection): collection(_collection) {}
+    TaskArguments(const Akonadi::Collection &_collection, const Akonadi::Collection &_parentCollection): collection(_collection), parentCollection(_parentCollection) {}
+    TaskArguments(const Akonadi::Collection &_collection, const Akonadi::Collection &_sourceCollection, const Akonadi::Collection &_targetCollection): collection(_collection), sourceCollection(_sourceCollection), targetCollection(_targetCollection) {}
+    TaskArguments(const Akonadi::Collection &_collection, const QSet<QByteArray> &_parts): collection(_collection), parts(_parts) {}
+    TaskArguments(const Akonadi::Tag &_tag) : tag(_tag) {}
+    Akonadi::Collection collection;
+    Akonadi::Item::List items;
+    Akonadi::Collection parentCollection; //only used as parent of a collection
+    Akonadi::Collection sourceCollection;
+    Akonadi::Collection targetCollection;
+    Akonadi::Tag tag;
+    QSet<QByteArray> parts;
+    QSet<QByteArray> addedFlags;
+    QSet<QByteArray> removedFlags;
+    QSet<Akonadi::Tag> addedTags;
+    QSet<Akonadi::Tag> removedTags;
+    Akonadi::Relation::List addedRelations;
+    Akonadi::Relation::List removedRelations;
+};
+
+class ResourceState : public ResourceStateInterface
+{
+public:
+    explicit ResourceState(ImapResourceBase *resource, const TaskArguments &arguments);
+
+public:
+    ~ResourceState();
+
+    QString userName() const Q_DECL_OVERRIDE;
+    QString resourceName() const Q_DECL_OVERRIDE;
+    QString resourceIdentifier() const Q_DECL_OVERRIDE;
+    QStringList serverCapabilities() const Q_DECL_OVERRIDE;
+    QList<KIMAP::MailBoxDescriptor> serverNamespaces() const Q_DECL_OVERRIDE;
+    QList<KIMAP::MailBoxDescriptor> personalNamespaces() const Q_DECL_OVERRIDE;
+    QList<KIMAP::MailBoxDescriptor> userNamespaces() const Q_DECL_OVERRIDE;
+    QList<KIMAP::MailBoxDescriptor> sharedNamespaces() const Q_DECL_OVERRIDE;
+
+    bool isAutomaticExpungeEnabled() const Q_DECL_OVERRIDE;
+    bool isSubscriptionEnabled() const Q_DECL_OVERRIDE;
+    bool isDisconnectedModeEnabled() const Q_DECL_OVERRIDE;
+    int intervalCheckTime() const Q_DECL_OVERRIDE;
+
+    Akonadi::Collection collection() const Q_DECL_OVERRIDE;
+    Akonadi::Item item() const Q_DECL_OVERRIDE;
+    Akonadi::Item::List items() const Q_DECL_OVERRIDE;
+
+    Akonadi::Collection parentCollection() const Q_DECL_OVERRIDE;
+
+    Akonadi::Collection sourceCollection() const Q_DECL_OVERRIDE;
+    Akonadi::Collection targetCollection() const Q_DECL_OVERRIDE;
+
+    QSet<QByteArray> parts() const Q_DECL_OVERRIDE;
+    QSet<QByteArray> addedFlags() const Q_DECL_OVERRIDE;
+    QSet<QByteArray> removedFlags() const Q_DECL_OVERRIDE;
+
+    Akonadi::Tag tag() const Q_DECL_OVERRIDE;
+    QSet<Akonadi::Tag> addedTags() const Q_DECL_OVERRIDE;
+    QSet<Akonadi::Tag> removedTags() const Q_DECL_OVERRIDE;
+
+    Akonadi::Relation::List addedRelations() const Q_DECL_OVERRIDE;
+    Akonadi::Relation::List removedRelations() const Q_DECL_OVERRIDE;
+
+    QString rootRemoteId() const Q_DECL_OVERRIDE;
+
+    void setIdleCollection(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    void applyCollectionChanges(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+
+    void collectionAttributesRetrieved(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+
+    void itemRetrieved(const Akonadi::Item &item) Q_DECL_OVERRIDE;
+
+    void itemsRetrieved(const Akonadi::Item::List &items) Q_DECL_OVERRIDE;
+    void itemsRetrievedIncremental(const Akonadi::Item::List &changed, const Akonadi::Item::List &removed) Q_DECL_OVERRIDE;
+    void itemsRetrievalDone() Q_DECL_OVERRIDE;
+
+    void setTotalItems(int) Q_DECL_OVERRIDE;
+
+    void itemChangeCommitted(const Akonadi::Item &item) Q_DECL_OVERRIDE;
+    void itemsChangesCommitted(const Akonadi::Item::List &items) Q_DECL_OVERRIDE;
+
+    void collectionsRetrieved(const Akonadi::Collection::List &collections) Q_DECL_OVERRIDE;
+
+    void tagsRetrieved(const Akonadi::Tag::List &tags, const QHash<QString, Akonadi::Item::List> &) Q_DECL_OVERRIDE;
+    void relationsRetrieved(const Akonadi::Relation::List &tags) Q_DECL_OVERRIDE;
+
+    void collectionChangeCommitted(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+
+    void tagChangeCommitted(const Akonadi::Tag &tag) Q_DECL_OVERRIDE;
+
+    void changeProcessed() Q_DECL_OVERRIDE;
+
+    void searchFinished(const QVector<qint64> &result, bool isRid = true) Q_DECL_OVERRIDE;
+
+    void cancelTask(const QString &errorString) Q_DECL_OVERRIDE;
+    void deferTask() Q_DECL_OVERRIDE;
+    void restartItemRetrieval(Akonadi::Collection::Id col) Q_DECL_OVERRIDE;
+    void taskDone() Q_DECL_OVERRIDE;
+
+    void emitError(const QString &message) Q_DECL_OVERRIDE;
+    void emitWarning(const QString &message) Q_DECL_OVERRIDE;
+
+    void emitPercent(int percent) Q_DECL_OVERRIDE;
+
+    virtual void synchronizeCollection(Akonadi::Collection::Id);
+    void synchronizeCollectionTree() Q_DECL_OVERRIDE;
+    void scheduleConnectionAttempt() Q_DECL_OVERRIDE;
+
+    QChar separatorCharacter() const Q_DECL_OVERRIDE;
+    void setSeparatorCharacter(const QChar &separator) Q_DECL_OVERRIDE;
+
+    void showInformationDialog(const QString &message, const QString &title, const QString &dontShowAgainName) Q_DECL_OVERRIDE;
+
+    int batchSize() const Q_DECL_OVERRIDE;
+
+    MessageHelper::Ptr messageHelper() const Q_DECL_OVERRIDE;
+
+    void setItemMergingMode(Akonadi::ItemSync::MergeMode mergeMode) Q_DECL_OVERRIDE;
+
+private:
+    ImapResourceBase *m_resource;
+    const TaskArguments m_arguments;
+};
+
+#endif
diff --git a/resources/imap/resourcestateinterface.cpp b/resources/imap/resourcestateinterface.cpp
new file mode 100644 (file)
index 0000000..e62688e
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "resourcestateinterface.h"
+#include "imapresource_debug.h"
+
+ResourceStateInterface::~ResourceStateInterface()
+{
+
+}
+
+QString ResourceStateInterface::mailBoxForCollection(const Akonadi::Collection &collection, bool showWarnings)
+{
+    if (collection.remoteId().isEmpty()) {   //This should never happen, investigate why a collection without remoteId made it this far
+        if (showWarnings) {
+            qCWarning(IMAPRESOURCE_LOG) << "Got incomplete ancestor chain due to empty remoteId:" << collection;
+        }
+        return QString();
+    }
+
+    if (collection.parentCollection() == Akonadi::Collection::root()) {
+        /*if ( showWarnings  && collection.remoteId() != rootRemoteId())
+          qCWarning(IMAPRESOURCE_LOG) << "RID mismatch, is " << collection.remoteId() << " expected " << rootRemoteId();
+        */
+        return QStringLiteral("");   // see below, this intentionally not just QString()!
+    }
+    const QString parentMailbox = mailBoxForCollection(collection.parentCollection());
+    if (parentMailbox.isNull()) { // invalid, != isEmpty() here!
+        return QString();
+    }
+
+    const QString mailbox =  parentMailbox + collection.remoteId();
+    if (parentMailbox.isEmpty()) {
+        return mailbox.mid(1);    // strip of the separator on top-level mailboxes
+    }
+    return mailbox;
+}
diff --git a/resources/imap/resourcestateinterface.h b/resources/imap/resourcestateinterface.h
new file mode 100644 (file)
index 0000000..7040c7a
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef RESOURCESTATEINTERFACE_H
+#define RESOURCESTATEINTERFACE_H
+
+#include <QtCore/QStringList>
+#include <QtCore/QSharedPointer>
+
+#include <Collection>
+#include <Item>
+#include <ItemSync>
+
+#include <kimap/listjob.h>
+
+#include "messagehelper.h"
+
+class ResourceStateInterface
+{
+public:
+    typedef QSharedPointer<ResourceStateInterface> Ptr;
+
+    virtual ~ResourceStateInterface();
+
+    virtual QString userName() const = 0;
+    virtual QString resourceName() const = 0;
+    virtual QString resourceIdentifier() const = 0;
+    virtual QStringList serverCapabilities() const = 0;
+    virtual QList<KIMAP::MailBoxDescriptor> serverNamespaces() const = 0;
+    virtual QList<KIMAP::MailBoxDescriptor> personalNamespaces() const = 0;
+    virtual QList<KIMAP::MailBoxDescriptor> userNamespaces() const = 0;
+    virtual QList<KIMAP::MailBoxDescriptor> sharedNamespaces() const = 0;
+
+    virtual bool isAutomaticExpungeEnabled() const = 0;
+    virtual bool isSubscriptionEnabled() const = 0;
+    virtual bool isDisconnectedModeEnabled() const = 0;
+    virtual int intervalCheckTime() const = 0;
+
+    virtual Akonadi::Collection collection() const = 0;
+    virtual Akonadi::Item item() const = 0;
+    virtual Akonadi::Item::List items() const = 0;
+
+    virtual Akonadi::Collection parentCollection() const = 0;
+
+    virtual Akonadi::Collection sourceCollection() const = 0;
+    virtual Akonadi::Collection targetCollection() const = 0;
+
+    virtual QSet<QByteArray> parts() const = 0;
+    virtual QSet<QByteArray> addedFlags() const = 0;
+    virtual QSet<QByteArray> removedFlags() const = 0;
+
+    virtual Akonadi::Tag tag() const = 0;
+    virtual QSet<Akonadi::Tag> addedTags() const = 0;
+    virtual QSet<Akonadi::Tag> removedTags() const = 0;
+
+    virtual QString rootRemoteId() const = 0;
+    static QString mailBoxForCollection(const Akonadi::Collection &collection, bool showWarnings = true);
+
+    virtual void setIdleCollection(const Akonadi::Collection &collection) = 0;
+    virtual void applyCollectionChanges(const Akonadi::Collection &collection) = 0;
+
+    virtual void itemRetrieved(const Akonadi::Item &item) = 0;
+
+    virtual void itemsRetrieved(const Akonadi::Item::List &items) = 0;
+    virtual void itemsRetrievedIncremental(const Akonadi::Item::List &changed,
+                                           const Akonadi::Item::List &removed) = 0;
+    virtual void itemsRetrievalDone() = 0;
+
+    virtual void setTotalItems(int) = 0;
+
+    virtual void itemChangeCommitted(const Akonadi::Item &item) = 0;
+    virtual void itemsChangesCommitted(const Akonadi::Item::List &items) = 0;
+
+    virtual void collectionsRetrieved(const Akonadi::Collection::List &collections) = 0;
+    virtual void collectionAttributesRetrieved(const Akonadi::Collection &collection) = 0;
+
+    virtual void collectionChangeCommitted(const Akonadi::Collection &collection) = 0;
+
+    virtual void tagChangeCommitted(const Akonadi::Tag &tag) = 0;
+
+    virtual void changeProcessed() = 0;
+
+    virtual void searchFinished(const QVector<qint64> &result, bool isRid = true) = 0;
+
+    virtual void cancelTask(const QString &errorString) = 0;
+    virtual void deferTask() = 0;
+    virtual void restartItemRetrieval(Akonadi::Collection::Id col) = 0;
+    virtual void taskDone() = 0;
+
+    virtual void emitError(const QString &message) = 0;
+    virtual void emitWarning(const QString &message) = 0;
+    virtual void emitPercent(int percent) = 0;
+
+    virtual void synchronizeCollectionTree() = 0;
+    virtual void scheduleConnectionAttempt() = 0;
+
+    virtual QChar separatorCharacter() const = 0;
+    virtual void setSeparatorCharacter(const QChar &separator) = 0;
+
+    virtual void showInformationDialog(const QString &message, const QString &title, const QString &dontShowAgainName) = 0;
+
+    virtual int batchSize() const = 0;
+
+    virtual MessageHelper::Ptr messageHelper() const = 0;
+
+    virtual void tagsRetrieved(const Akonadi::Tag::List &tags, const QHash<QString, Akonadi::Item::List> &) = 0;
+    virtual void relationsRetrieved(const Akonadi::Relation::List &tags) = 0;
+
+    virtual Akonadi::Relation::List addedRelations() const = 0;
+    virtual Akonadi::Relation::List removedRelations() const = 0;
+
+    virtual void setItemMergingMode(Akonadi::ItemSync::MergeMode mergeMode) = 0;
+};
+
+#endif
diff --git a/resources/imap/resourcetask.cpp b/resources/imap/resourcetask.cpp
new file mode 100644 (file)
index 0000000..0710e28
--- /dev/null
@@ -0,0 +1,598 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "resourcetask.h"
+
+#include <Akonadi/KMime/MessageFlags>
+
+#include <KLocalizedString>
+#include "imapresource_debug.h"
+
+#include "collectionflagsattribute.h"
+#include <imapaclattribute.h>
+#include "imapflags.h"
+#include "sessionpool.h"
+#include "resourcestateinterface.h"
+#include "tracer.h"
+
+ResourceTask::ResourceTask(ActionIfNoSession action, ResourceStateInterface::Ptr resource, QObject *parent)
+    : QObject(parent),
+      m_pool(Q_NULLPTR),
+      m_sessionRequestId(0),
+      m_session(Q_NULLPTR),
+      m_actionIfNoSession(action),
+      m_resource(resource),
+      mCancelled(false)
+{
+
+}
+
+ResourceTask::~ResourceTask()
+{
+    if (m_pool) {
+        if (m_sessionRequestId) {
+            m_pool->cancelSessionRequest(m_sessionRequestId);
+        }
+        if (m_session) {
+            m_pool->releaseSession(m_session);
+        }
+    }
+}
+
+void ResourceTask::start(SessionPool *pool)
+{
+    Trace() << metaObject()->className();
+    m_pool = pool;
+    connect(m_pool, &SessionPool::sessionRequestDone,
+            this, &ResourceTask::onSessionRequested);
+
+    m_sessionRequestId = m_pool->requestSession();
+
+    if (m_sessionRequestId <= 0) {
+        m_sessionRequestId = 0;
+
+        switch (m_actionIfNoSession) {
+        case CancelIfNoSession:
+            qCDebug(IMAPRESOURCE_LOG) << "Cancelling this request. Probably there is no connection.";
+            m_resource->cancelTask(i18n("There is currently no connection to the IMAP server."));
+            break;
+
+        case DeferIfNoSession:
+            qCDebug(IMAPRESOURCE_LOG) << "Defering this request. Probably there is no connection.";
+            m_resource->deferTask();
+            break;
+        }
+
+        // In this case we were likely disconnect, try to get the resource online
+        m_resource->scheduleConnectionAttempt();
+        deleteLater();
+    }
+}
+
+void ResourceTask::onSessionRequested(qint64 requestId, KIMAP::Session *session,
+                                      int errorCode, const QString &/*errorString*/)
+{
+    if (requestId != m_sessionRequestId) {
+        // Not for us, ignore
+        return;
+    }
+
+    disconnect(m_pool, &SessionPool::sessionRequestDone,
+               this, &ResourceTask::onSessionRequested);
+    m_sessionRequestId = 0;
+
+    if (errorCode != SessionPool::NoError) {
+        switch (m_actionIfNoSession) {
+        case CancelIfNoSession:
+            qCDebug(IMAPRESOURCE_LOG) << "Cancelling this request. Probably there is no more session available.";
+            m_resource->cancelTask(i18n("There is currently no session to the IMAP server available."));
+            break;
+
+        case DeferIfNoSession:
+            qCDebug(IMAPRESOURCE_LOG) << "Defering this request. Probably there is no more session available.";
+            m_resource->deferTask();
+            break;
+        }
+
+        deleteLater();
+        return;
+    }
+
+    m_session = session;
+
+    if (errorCode != SessionPool::NoError) {
+        Trace() << "Error on: " << metaObject()->className();
+        switch (m_actionIfNoSession) {
+        case CancelIfNoSession:
+            qCDebug(IMAPRESOURCE_LOG) << "Cancelling this request. Probably there is no more session available.";
+            m_resource->cancelTask(i18n("There is currently no session to the IMAP server available."));
+            break;
+
+        case DeferIfNoSession:
+            qCDebug(IMAPRESOURCE_LOG) << "Defering this request. Probably there is no more session available.";
+            m_resource->deferTask();
+            break;
+        }
+
+        deleteLater();
+        return;
+    }
+
+    m_session = session;
+
+    connect(m_pool, &SessionPool::connectionLost,
+            this, &ResourceTask::onConnectionLost);
+    connect(m_pool, &SessionPool::disconnectDone,
+            this, &ResourceTask::onPoolDisconnect);
+
+    Trace() << "starting: " << metaObject()->className();
+    doStart(m_session);
+}
+
+void ResourceTask::onConnectionLost(KIMAP::Session *session)
+{
+    if (session == m_session) {
+        // Our session becomes invalid, so get rid of
+        // the pointer, we don't need to release it once the
+        // task is done
+        m_session = Q_NULLPTR;
+        Trace() << metaObject()->className();
+        cancelTask(i18n("Connection lost"));
+    }
+}
+
+void ResourceTask::onPoolDisconnect()
+{
+    // All the sessions in the pool we used changed,
+    // so get rid of the pointer, we don't need to
+    // release our session anymore
+    m_pool = Q_NULLPTR;
+
+    Trace() << metaObject()->className();
+    cancelTask(i18n("Connection lost"));
+}
+
+QString ResourceTask::userName() const
+{
+    return m_resource->userName();
+}
+
+QString ResourceTask::resourceName() const
+{
+    return m_resource->resourceName();
+}
+
+QStringList ResourceTask::serverCapabilities() const
+{
+    return m_resource->serverCapabilities();
+}
+
+QList<KIMAP::MailBoxDescriptor> ResourceTask::serverNamespaces() const
+{
+    return m_resource->serverNamespaces();
+}
+
+bool ResourceTask::isAutomaticExpungeEnabled() const
+{
+    return m_resource->isAutomaticExpungeEnabled();
+}
+
+bool ResourceTask::isSubscriptionEnabled() const
+{
+    return m_resource->isSubscriptionEnabled();
+}
+
+bool ResourceTask::isDisconnectedModeEnabled() const
+{
+    return m_resource->isDisconnectedModeEnabled();
+}
+
+int ResourceTask::intervalCheckTime() const
+{
+    return m_resource->intervalCheckTime();
+}
+
+static Akonadi::Collection detatchCollection(const Akonadi::Collection &collection)
+{
+    //HACK: Attributes are accessed via a const function, and the implicitly shared private pointer thus doesn't detach.
+    //We force a detach to avoid surprises. (RetrieveItemsTask used to write back the collection changes, even though the task was canceled)
+    //Once this is fixed this function can go away.
+    Akonadi::Collection col = collection;
+    col.setId(col.id());
+    return col;
+}
+
+Akonadi::Collection ResourceTask::collection() const
+{
+    return detatchCollection(m_resource->collection());
+}
+
+Akonadi::Item ResourceTask::item() const
+{
+    return m_resource->item();
+}
+
+Akonadi::Item::List ResourceTask::items() const
+{
+    return m_resource->items();
+}
+
+Akonadi::Collection ResourceTask::parentCollection() const
+{
+    return detatchCollection(m_resource->parentCollection());
+}
+
+Akonadi::Collection ResourceTask::sourceCollection() const
+{
+    return detatchCollection(m_resource->sourceCollection());
+}
+
+Akonadi::Collection ResourceTask::targetCollection() const
+{
+    return detatchCollection(m_resource->targetCollection());
+}
+
+QSet<QByteArray> ResourceTask::parts() const
+{
+    return m_resource->parts();
+}
+
+QSet< QByteArray > ResourceTask::addedFlags() const
+{
+    return m_resource->addedFlags();
+}
+
+QSet< QByteArray > ResourceTask::removedFlags() const
+{
+    return m_resource->removedFlags();
+}
+
+QString ResourceTask::rootRemoteId() const
+{
+    return m_resource->rootRemoteId();
+}
+
+QString ResourceTask::mailBoxForCollection(const Akonadi::Collection &collection) const
+{
+    return m_resource->mailBoxForCollection(collection);
+}
+
+void ResourceTask::setIdleCollection(const Akonadi::Collection &collection)
+{
+    if (!mCancelled) {
+        m_resource->setIdleCollection(collection);
+    }
+}
+
+void ResourceTask::applyCollectionChanges(const Akonadi::Collection &collection)
+{
+    if (!mCancelled) {
+        m_resource->applyCollectionChanges(collection);
+    }
+}
+
+void ResourceTask::itemRetrieved(const Akonadi::Item &item)
+{
+    if (!mCancelled) {
+        m_resource->itemRetrieved(item);
+        emitPercent(100);
+    }
+    deleteLater();
+}
+
+void ResourceTask::itemsRetrieved(const Akonadi::Item::List &items)
+{
+    if (!mCancelled) {
+        m_resource->itemsRetrieved(items);
+    }
+}
+
+void ResourceTask::itemsRetrievedIncremental(const Akonadi::Item::List &changed,
+        const Akonadi::Item::List &removed)
+{
+    if (!mCancelled) {
+        m_resource->itemsRetrievedIncremental(changed, removed);
+    }
+}
+
+void ResourceTask::itemsRetrievalDone()
+{
+    if (!mCancelled) {
+        m_resource->itemsRetrievalDone();
+    }
+    deleteLater();
+}
+
+void ResourceTask::setTotalItems(int totalItems)
+{
+    if (!mCancelled) {
+        m_resource->setTotalItems(totalItems);
+    }
+}
+
+void ResourceTask::changeCommitted(const Akonadi::Item &item)
+{
+    if (!mCancelled) {
+        m_resource->itemChangeCommitted(item);
+    }
+    deleteLater();
+}
+
+void ResourceTask::changesCommitted(const Akonadi::Item::List &items)
+{
+    if (!mCancelled) {
+        m_resource->itemsChangesCommitted(items);
+    }
+    deleteLater();
+}
+
+void ResourceTask::searchFinished(const QVector<qint64> &result, bool isRid)
+{
+    if (!mCancelled) {
+        m_resource->searchFinished(result, isRid);
+    }
+    deleteLater();
+}
+
+void ResourceTask::collectionsRetrieved(const Akonadi::Collection::List &collections)
+{
+    if (!mCancelled) {
+        m_resource->collectionsRetrieved(collections);
+    }
+    deleteLater();
+}
+
+void ResourceTask::collectionAttributesRetrieved(const Akonadi::Collection &col)
+{
+    if (!mCancelled) {
+        m_resource->collectionAttributesRetrieved(col);
+    }
+    deleteLater();
+}
+
+void ResourceTask::changeCommitted(const Akonadi::Collection &collection)
+{
+    if (!mCancelled) {
+        m_resource->collectionChangeCommitted(collection);
+    }
+    deleteLater();
+}
+
+void ResourceTask::changeCommitted(const Akonadi::Tag &tag)
+{
+    if (!mCancelled) {
+        m_resource->tagChangeCommitted(tag);
+    }
+    deleteLater();
+}
+
+void ResourceTask::changeProcessed()
+{
+    if (!mCancelled) {
+        m_resource->changeProcessed();
+    }
+    deleteLater();
+}
+
+void ResourceTask::cancelTask(const QString &errorString)
+{
+    qCDebug(IMAPRESOURCE_LOG) << "Cancel task: " << errorString;
+    if (!mCancelled) {
+        mCancelled = true;
+        m_resource->cancelTask(errorString);
+    }
+    deleteLater();
+}
+
+void ResourceTask::deferTask()
+{
+    if (!mCancelled) {
+        mCancelled = true;
+        m_resource->deferTask();
+    }
+    deleteLater();
+}
+
+void ResourceTask::restartItemRetrieval(Akonadi::Collection::Id col)
+{
+    if (!mCancelled) {
+        m_resource->restartItemRetrieval(col);
+    }
+    deleteLater();
+}
+
+void ResourceTask::taskDone()
+{
+    m_resource->taskDone();
+    deleteLater();
+}
+
+void ResourceTask::emitPercent(int percent)
+{
+    m_resource->emitPercent(percent);
+}
+
+void ResourceTask::emitError(const QString &message)
+{
+    m_resource->emitError(message);
+}
+
+void ResourceTask::emitWarning(const QString &message)
+{
+    m_resource->emitWarning(message);
+}
+
+void ResourceTask::synchronizeCollectionTree()
+{
+    m_resource->synchronizeCollectionTree();
+}
+
+void ResourceTask::showInformationDialog(const QString &message, const QString &title, const QString &dontShowAgainName)
+{
+    m_resource->showInformationDialog(message, title, dontShowAgainName);
+}
+
+QList<QByteArray> ResourceTask::fromAkonadiToSupportedImapFlags(const QList<QByteArray> &flags,
+        const Akonadi::Collection &collection)
+{
+    QList<QByteArray> imapFlags = fromAkonadiFlags(flags);
+
+    const Akonadi::CollectionFlagsAttribute *flagAttr = collection.attribute<Akonadi::CollectionFlagsAttribute>();
+    // the server does not support arbitrary flags, so filter out those it can't handle
+    if (flagAttr && !flagAttr->flags().isEmpty() && !flagAttr->flags().contains("\\*")) {
+        for (QList< QByteArray >::iterator it = imapFlags.begin(); it != imapFlags.end();) {
+            if (flagAttr->flags().contains(*it)) {
+                ++it;
+            } else {
+                qCDebug(IMAPRESOURCE_LOG) << "Server does not support flag" << *it;
+                it = imapFlags.erase(it);
+            }
+        }
+    }
+
+    return imapFlags;
+}
+
+QList<QByteArray> ResourceTask::fromAkonadiFlags(const QList<QByteArray> &flags)
+{
+    QList<QByteArray> newFlags;
+
+    foreach (const QByteArray &oldFlag, flags) {
+        if (oldFlag == Akonadi::MessageFlags::Seen) {
+            newFlags.append(ImapFlags::Seen);
+        } else if (oldFlag == Akonadi::MessageFlags::Deleted) {
+            newFlags.append(ImapFlags::Deleted);
+        } else if (oldFlag == Akonadi::MessageFlags::Answered || oldFlag == Akonadi::MessageFlags::Replied) {
+            newFlags.append(ImapFlags::Answered);
+        } else if (oldFlag == Akonadi::MessageFlags::Flagged) {
+            newFlags.append(ImapFlags::Flagged);
+        } else {
+            newFlags.append(oldFlag);
+        }
+    }
+
+    return newFlags;
+}
+
+QList<QByteArray> ResourceTask::toAkonadiFlags(const QList<QByteArray> &flags)
+{
+    QList<QByteArray> newFlags;
+
+    foreach (const QByteArray &oldFlag, flags) {
+        if (oldFlag == ImapFlags::Seen) {
+            newFlags.append(Akonadi::MessageFlags::Seen);
+        } else if (oldFlag == ImapFlags::Deleted) {
+            newFlags.append(Akonadi::MessageFlags::Deleted);
+        } else if (oldFlag == ImapFlags::Answered) {
+            newFlags.append(Akonadi::MessageFlags::Answered);
+        } else if (oldFlag == ImapFlags::Flagged) {
+            newFlags.append(Akonadi::MessageFlags::Flagged);
+        } else if (oldFlag.isEmpty()) {
+            // filter out empty flags, to avoid isNull/isEmpty confusions higher up
+            continue;
+        } else {
+            newFlags.append(oldFlag);
+        }
+    }
+
+    return newFlags;
+}
+
+void ResourceTask::kill()
+{
+    Trace() << metaObject()->className();
+    qCDebug(IMAPRESOURCE_LOG);
+    cancelTask(i18n("killed"));
+}
+
+const QChar ResourceTask::separatorCharacter() const
+{
+    const QChar separator = m_resource->separatorCharacter();
+    if (!separator.isNull()) {
+        return separator;
+    } else {
+        //If we request the separator before first folder listing, then try to guess
+        //the separator:
+        //If we create a toplevel folder, assume the separator to be '/'. This is not perfect, but detecting the right
+        //IMAP separator is not straightforward for toplevel folders, and fixes bug 292418 and maybe other, where
+        //subfolders end up with remote id's starting with "i" (the first letter of imap:// ...)
+
+        QString remoteId;
+        // We don't always have parent collection set (for example for CollectionChangeTask),
+        // in such cases however we can use current collection's remoteId to get the separator
+        const Akonadi::Collection parent = parentCollection();
+        if (parent.isValid()) {
+            remoteId = parent.remoteId();
+        } else {
+            remoteId = collection().remoteId();
+        }
+        return ((remoteId != rootRemoteId()) && !remoteId.isEmpty()) ? remoteId.at(0) : QLatin1Char('/');
+    }
+}
+
+void ResourceTask::setSeparatorCharacter(const QChar &separator)
+{
+    m_resource->setSeparatorCharacter(separator);
+}
+
+bool ResourceTask::serverSupportsAnnotations() const
+{
+    return serverCapabilities().contains(QStringLiteral("METADATA"))
+           || serverCapabilities().contains(QStringLiteral("ANNOTATEMORE"));
+}
+
+bool ResourceTask::serverSupportsCondstore() const
+{
+    // Don't enable CONDSTORE for GMail (X-GM-EXT-1 is a GMail-specific capability)
+    // because it breaks changes synchronization when using labels.
+    return serverCapabilities().contains(QStringLiteral("CONDSTORE")) &&
+           !serverCapabilities().contains(QStringLiteral("X-GM-EXT-1"));
+}
+
+int ResourceTask::batchSize() const
+{
+    return m_resource->batchSize();
+}
+
+ResourceStateInterface::Ptr ResourceTask::resourceState()
+{
+    return m_resource;
+}
+
+KIMAP::Acl::Rights ResourceTask::myRights(const Akonadi::Collection &col)
+{
+    Akonadi::ImapAclAttribute *aclAttribute = col.attribute<Akonadi::ImapAclAttribute>();
+    if (aclAttribute) {
+        //HACK, only return myrights if they are available
+        if (aclAttribute->myRights() != KIMAP::Acl::None) {
+            return aclAttribute->myRights();
+        } else {
+            //This should be removed after 4.14, and myrights should be always used.
+            return aclAttribute->rights().value(userName().toUtf8());
+        }
+    }
+    return KIMAP::Acl::None;
+}
+
+void ResourceTask::setItemMergingMode(Akonadi::ItemSync::MergeMode mode)
+{
+    m_resource->setItemMergingMode(mode);
+}
diff --git a/resources/imap/resourcetask.h b/resources/imap/resourcetask.h
new file mode 100644 (file)
index 0000000..d034c9d
--- /dev/null
@@ -0,0 +1,169 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef RESOURCETASK_H
+#define RESOURCETASK_H
+
+#include <QtCore/QObject>
+
+#include <Collection>
+#include <Item>
+
+#include <kimap/listjob.h>
+#include <kimap/acl.h>
+
+#include "resourcestateinterface.h"
+
+namespace KIMAP
+{
+class Session;
+}
+
+class SessionPool;
+
+class ResourceTask : public QObject
+{
+    Q_OBJECT
+    Q_ENUMS(ActionIfNoSession)
+
+public:
+    enum ActionIfNoSession {
+        CancelIfNoSession,
+        DeferIfNoSession
+    };
+
+    explicit ResourceTask(ActionIfNoSession action, ResourceStateInterface::Ptr resource, QObject *parent = Q_NULLPTR);
+    virtual ~ResourceTask();
+
+    void start(SessionPool *pool);
+
+    void kill();
+
+    static QList<QByteArray> fromAkonadiToSupportedImapFlags(const QList<QByteArray> &flags, const Akonadi::Collection &collection);
+
+    static QList<QByteArray> toAkonadiFlags(const QList<QByteArray> &flags);
+
+Q_SIGNALS:
+    void status(int status, const QString &message = QString());
+
+protected:
+    virtual void doStart(KIMAP::Session *session) = 0;
+
+protected:
+    QString userName() const;
+    QString resourceName() const;
+    QStringList serverCapabilities() const;
+    QList<KIMAP::MailBoxDescriptor> serverNamespaces() const;
+
+    bool isAutomaticExpungeEnabled() const;
+    bool isSubscriptionEnabled() const;
+    bool isDisconnectedModeEnabled() const;
+    int intervalCheckTime() const;
+
+    Akonadi::Collection collection() const;
+    Akonadi::Item item() const;
+    Akonadi::Item::List items() const;
+
+    Akonadi::Collection parentCollection() const;
+
+    Akonadi::Collection sourceCollection() const;
+    Akonadi::Collection targetCollection() const;
+
+    QSet<QByteArray> parts() const;
+    QSet<QByteArray> addedFlags() const;
+    QSet<QByteArray> removedFlags() const;
+
+    QString rootRemoteId() const;
+    QString mailBoxForCollection(const Akonadi::Collection &collection) const;
+
+    void setIdleCollection(const Akonadi::Collection &collection);
+    void applyCollectionChanges(const Akonadi::Collection &collection);
+
+    void itemRetrieved(const Akonadi::Item &item);
+
+    void itemsRetrieved(const Akonadi::Item::List &items);
+    void itemsRetrievedIncremental(const Akonadi::Item::List &changed,
+                                   const Akonadi::Item::List &removed);
+    void itemsRetrievalDone();
+
+    void setTotalItems(int);
+
+    void changeCommitted(const Akonadi::Item &item);
+    void changesCommitted(const Akonadi::Item::List &items);
+
+    void collectionsRetrieved(const Akonadi::Collection::List &collections);
+
+    void collectionAttributesRetrieved(const Akonadi::Collection &col);
+
+    void changeCommitted(const Akonadi::Collection &collection);
+
+    void changeCommitted(const Akonadi::Tag &tag);
+
+    void changeProcessed();
+
+    void searchFinished(const QVector<qint64> &result, bool isRid = true);
+
+    void cancelTask(const QString &errorString);
+    void deferTask();
+    void restartItemRetrieval(Akonadi::Collection::Id col);
+    void taskDone();
+    void emitPercent(int percent);
+    void emitError(const QString &message);
+    void emitWarning(const QString &message);
+
+    void synchronizeCollectionTree();
+
+    void showInformationDialog(const QString &message, const QString &title, const QString &dontShowAgainName);
+
+    const QChar separatorCharacter() const;
+    void setSeparatorCharacter(const QChar &separator);
+
+    virtual bool serverSupportsAnnotations() const;
+    virtual bool serverSupportsCondstore() const;
+
+    int batchSize() const;
+    void setItemMergingMode(Akonadi::ItemSync::MergeMode mode);
+
+    ResourceStateInterface::Ptr resourceState();
+
+    KIMAP::Acl::Rights myRights(const Akonadi::Collection &);
+
+private:
+
+    static QList<QByteArray> fromAkonadiFlags(const QList<QByteArray> &flags);
+
+private Q_SLOTS:
+    void onSessionRequested(qint64 requestId, KIMAP::Session *session,
+                            int errorCode, const QString &errorString);
+    void onConnectionLost(KIMAP::Session *session);
+    void onPoolDisconnect();
+
+private:
+    SessionPool *m_pool;
+    qint64 m_sessionRequestId;
+
+    KIMAP::Session *m_session;
+    ActionIfNoSession m_actionIfNoSession;
+    ResourceStateInterface::Ptr m_resource;
+    bool mCancelled;
+};
+
+#endif
diff --git a/resources/imap/retrievecollectionmetadatatask.cpp b/resources/imap/retrievecollectionmetadatatask.cpp
new file mode 100644 (file)
index 0000000..7d7d460
--- /dev/null
@@ -0,0 +1,292 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "retrievecollectionmetadatatask.h"
+
+#include <QtCore/QDateTime>
+
+#include <kimap/getacljob.h>
+#include <kimap/getmetadatajob.h>
+#include <kimap/getquotarootjob.h>
+#include <kimap/myrightsjob.h>
+#include <kimap/rfccodecs.h>
+#include <kimap/session.h>
+#include <KLocalizedString>
+
+#include "imapresource_debug.h"
+
+#include <collectionquotaattribute.h>
+#include <entitydisplayattribute.h>
+#include "collectionannotationsattribute.h"
+#include "imapaclattribute.h"
+#include "imapquotaattribute.h"
+#include "noselectattribute.h"
+#include "collectionmetadatahelper.h"
+
+RetrieveCollectionMetadataTask::RetrieveCollectionMetadataTask(const ResourceStateInterface::Ptr &resource, QObject *parent)
+    : ResourceTask(CancelIfNoSession, resource, parent),
+      m_pendingMetaDataJobs(0),
+      m_session(Q_NULLPTR)
+{
+}
+
+RetrieveCollectionMetadataTask::~RetrieveCollectionMetadataTask()
+{
+}
+
+void RetrieveCollectionMetadataTask::doStart(KIMAP::Session *session)
+{
+    qCDebug(IMAPRESOURCE_LOG) << collection().remoteId();
+
+    // Prevent fetching metadata from noselect folders.
+    if (collection().hasAttribute("noselect")) {
+        NoSelectAttribute *noselect = static_cast<NoSelectAttribute *>(collection().attribute("noselect"));
+        if (noselect->noSelect()) {
+            qCDebug(IMAPRESOURCE_LOG) << "No Select folder";
+            endTaskIfNeeded();
+            return;
+        }
+    }
+
+    m_session = session;
+    m_collection = collection();
+    const QString mailBox = mailBoxForCollection(m_collection);
+    const QStringList capabilities = serverCapabilities();
+
+    m_pendingMetaDataJobs = 0;
+
+    // First get the annotations from the mailbox if it's supported
+    if (capabilities.contains(QStringLiteral("METADATA")) || capabilities.contains(QStringLiteral("ANNOTATEMORE"))) {
+        KIMAP::GetMetaDataJob *meta = new KIMAP::GetMetaDataJob(session);
+        meta->setMailBox(mailBox);
+        if (capabilities.contains(QStringLiteral("METADATA"))) {
+            meta->setServerCapability(KIMAP::MetaDataJobBase::Metadata);
+            meta->addRequestedEntry("/shared");
+            meta->setDepth(KIMAP::GetMetaDataJob::AllLevels);
+        } else {
+            meta->setServerCapability(KIMAP::MetaDataJobBase::Annotatemore);
+            meta->addEntry("*", "value.shared");
+        }
+        connect(meta, &KJob::result, this, &RetrieveCollectionMetadataTask::onGetMetaDataDone);
+        m_pendingMetaDataJobs++;
+        meta->start();
+    }
+
+    // Get the ACLs from the mailbox if it's supported
+    if (capabilities.contains(QStringLiteral("ACL"))) {
+        KIMAP::MyRightsJob *rights = new KIMAP::MyRightsJob(session);
+        rights->setMailBox(mailBox);
+        connect(rights, &KJob::result, this, &RetrieveCollectionMetadataTask::onRightsReceived);
+        m_pendingMetaDataJobs++;
+        rights->start();
+    }
+
+    // Get the QUOTA info from the mailbox if it's supported
+    if (capabilities.contains(QStringLiteral("QUOTA"))) {
+        KIMAP::GetQuotaRootJob *quota = new KIMAP::GetQuotaRootJob(session);
+        quota->setMailBox(mailBox);
+        connect(quota, &KJob::result, this, &RetrieveCollectionMetadataTask::onQuotasReceived);
+        m_pendingMetaDataJobs++;
+        quota->start();
+    }
+
+    // the server does not have any of the capabilities needed to get extra info, so this
+    // step is done here
+    if (m_pendingMetaDataJobs == 0) {
+        endTaskIfNeeded();
+    }
+}
+
+void RetrieveCollectionMetadataTask::onGetMetaDataDone(KJob *job)
+{
+    m_pendingMetaDataJobs--;
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Get metadata failed: " << job->errorString();
+        endTaskIfNeeded();
+        return; // Well, no metadata for us then...
+    }
+
+    KIMAP::GetMetaDataJob *meta = qobject_cast<KIMAP::GetMetaDataJob *>(job);
+    QMap<QByteArray, QByteArray> rawAnnotations = meta->allMetaData();
+
+    // filter out unused and annoying Cyrus annotation /vendor/cmu/cyrus-imapd/lastupdate
+    // which contains the current date and time and thus constantly changes for no good
+    // reason which triggers a change notification and thus a bunch of Akonadi operations
+    rawAnnotations.remove("/shared/vendor/cmu/cyrus-imapd/lastupdate");
+    rawAnnotations.remove("/private/vendor/cmu/cyrus-imapd/lastupdate");
+
+    // Store the mailbox metadata
+    Akonadi::CollectionAnnotationsAttribute *annotationsAttribute =
+        m_collection.attribute<Akonadi::CollectionAnnotationsAttribute>(Akonadi::Collection::AddIfMissing);
+    const QMap<QByteArray, QByteArray> oldAnnotations = annotationsAttribute->annotations();
+    if (oldAnnotations != rawAnnotations) {
+        annotationsAttribute->setAnnotations(rawAnnotations);
+    }
+
+    endTaskIfNeeded();
+}
+
+void RetrieveCollectionMetadataTask::onGetAclDone(KJob *job)
+{
+    m_pendingMetaDataJobs--;
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "GetACL failed: " << job->errorString();
+        endTaskIfNeeded();
+        return; // Well, no metadata for us then...
+    }
+
+    KIMAP::GetAclJob *acl = qobject_cast<KIMAP::GetAclJob *>(job);
+
+    // Store the mailbox ACLs
+    Akonadi::ImapAclAttribute *const aclAttribute
+        = m_collection.attribute<Akonadi::ImapAclAttribute>(Akonadi::Collection::AddIfMissing);
+    const QMap<QByteArray, KIMAP::Acl::Rights> oldRights = aclAttribute->rights();
+    if (oldRights != acl->allRights()) {
+        aclAttribute->setRights(acl->allRights());
+    }
+
+    endTaskIfNeeded();
+}
+
+void RetrieveCollectionMetadataTask::onRightsReceived(KJob *job)
+{
+    m_pendingMetaDataJobs--;
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "MyRights failed: " << job->errorString();
+        endTaskIfNeeded();
+        return; // Well, no metadata for us then...
+    }
+
+    KIMAP::MyRightsJob *rightsJob = qobject_cast<KIMAP::MyRightsJob *>(job);
+
+    const KIMAP::Acl::Rights imapRights = rightsJob->rights();
+
+    //Default value in case we have nothing better available
+    KIMAP::Acl::Rights parentRights = KIMAP::Acl::CreateMailbox | KIMAP::Acl::Create;
+
+    //FIXME I don't think we have the parent's acl's available
+    if (collection().parentCollection().attribute<Akonadi::ImapAclAttribute>()) {
+        parentRights = myRights(collection().parentCollection());
+    }
+
+//  qCDebug(IMAPRESOURCE_LOG) << collection.remoteId()
+//                 << "imapRights:" << imapRights
+//                 << "newRights:" << newRights
+//                 << "oldRights:" << collection.rights();
+
+    const bool isNewCollection = !m_collection.hasAttribute<Akonadi::ImapAclAttribute>();
+    const bool accessRevoked = CollectionMetadataHelper::applyRights(m_collection, imapRights, parentRights);
+    if (accessRevoked && !isNewCollection) {
+        // write access revoked
+        const QString collectionName = m_collection.displayName();
+
+        showInformationDialog(i18n("<p>Your access rights to folder <b>%1</b> have been restricted, "
+                                   "it will no longer be possible to add messages to this folder.</p>",
+                                   collectionName),
+                              i18n("Access rights revoked"), QStringLiteral("ShowRightsRevokedWarning"));
+    }
+
+    // Store the mailbox ACLs
+    Akonadi::ImapAclAttribute *aclAttribute
+        = m_collection.attribute<Akonadi::ImapAclAttribute>(Akonadi::Collection::AddIfMissing);
+    const KIMAP::Acl::Rights oldRights = aclAttribute->myRights();
+    if (oldRights != imapRights) {
+        aclAttribute->setMyRights(imapRights);
+    }
+
+    //The a right is required to list acl's
+    if (imapRights & KIMAP::Acl::Admin) {
+        KIMAP::GetAclJob *acl = new KIMAP::GetAclJob(m_session);
+        acl->setMailBox(mailBoxForCollection(m_collection));
+        connect(acl, &KJob::result, this, &RetrieveCollectionMetadataTask::onGetAclDone);
+        m_pendingMetaDataJobs++;
+        acl->start();
+    }
+
+    endTaskIfNeeded();
+}
+
+void RetrieveCollectionMetadataTask::onQuotasReceived(KJob *job)
+{
+    m_pendingMetaDataJobs--;
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Quota retrieval failed: " << job->errorString();
+        endTaskIfNeeded();
+        return; // Well, no metadata for us then...
+    }
+
+    KIMAP::GetQuotaRootJob *quotaJob = qobject_cast<KIMAP::GetQuotaRootJob *>(job);
+    const QString &mailBox = mailBoxForCollection(m_collection);
+
+    QList<QByteArray> newRoots = quotaJob->roots();
+    QList< QMap<QByteArray, qint64> > newLimits;
+    QList< QMap<QByteArray, qint64> > newUsages;
+    qint64 newCurrent = -1;
+    qint64 newMax = -1;
+    newLimits.reserve(newRoots.count());
+    newUsages.reserve(newRoots.count());
+
+    foreach (const QByteArray &root, newRoots) {
+        newLimits << quotaJob->allLimits(root);
+        newUsages << quotaJob->allUsages(root);
+
+        const QString &decodedRoot = QString::fromUtf8(KIMAP::decodeImapFolderName(root));
+
+        if (newRoots.size() == 1 || decodedRoot == mailBox) {
+            newCurrent = newUsages.last()["STORAGE"] * 1024;
+            newMax = newLimits.last()["STORAGE"] * 1024;
+        }
+    }
+
+    // Store the mailbox IMAP Quotas
+    Akonadi::ImapQuotaAttribute *imapQuotaAttribute
+        = m_collection.attribute<Akonadi::ImapQuotaAttribute>(Akonadi::Collection::AddIfMissing);
+    const QList<QByteArray> oldRoots = imapQuotaAttribute->roots();
+    const QList< QMap<QByteArray, qint64> > oldLimits = imapQuotaAttribute->limits();
+    const QList< QMap<QByteArray, qint64> > oldUsages = imapQuotaAttribute->usages();
+
+    if (oldRoots != newRoots
+            || oldLimits != newLimits
+            || oldUsages != newUsages) {
+        imapQuotaAttribute->setQuotas(newRoots, newLimits, newUsages);
+    }
+
+    // Store the collection Quota
+    Akonadi::CollectionQuotaAttribute *quotaAttribute
+        = m_collection.attribute<Akonadi::CollectionQuotaAttribute>(Akonadi::Collection::AddIfMissing);
+    qint64 oldCurrent = quotaAttribute->currentValue();
+    qint64 oldMax = quotaAttribute->maximumValue();
+
+    if (oldCurrent != newCurrent
+            || oldMax != newMax) {
+        quotaAttribute->setCurrentValue(newCurrent);
+        quotaAttribute->setMaximumValue(newMax);
+    }
+
+    endTaskIfNeeded();
+}
+
+void RetrieveCollectionMetadataTask::endTaskIfNeeded()
+{
+    if (m_pendingMetaDataJobs <= 0) {
+        collectionAttributesRetrieved(m_collection);
+    }
+}
diff --git a/resources/imap/retrievecollectionmetadatatask.h b/resources/imap/retrievecollectionmetadatatask.h
new file mode 100644 (file)
index 0000000..bebd5b3
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef RETRIEVECOLLECTIONMETADATATASK_H
+#define RETRIEVECOLLECTIONMETADATATASK_H
+
+#include "resourcetask.h"
+
+class RetrieveCollectionMetadataTask : public ResourceTask
+{
+    Q_OBJECT
+
+public:
+    explicit RetrieveCollectionMetadataTask(const ResourceStateInterface::Ptr &resource, QObject *parent = Q_NULLPTR);
+    virtual ~RetrieveCollectionMetadataTask();
+
+private Q_SLOTS:
+    void onGetMetaDataDone(KJob *job);
+    void onGetAclDone(KJob *job);
+    void onRightsReceived(KJob *job);
+    void onQuotasReceived(KJob *job);
+
+protected:
+    void doStart(KIMAP::Session *session) Q_DECL_OVERRIDE;
+
+private:
+    void endTaskIfNeeded();
+
+    int m_pendingMetaDataJobs;
+
+    Akonadi::Collection m_collection;
+    KIMAP::Session *m_session;
+};
+
+#endif
diff --git a/resources/imap/retrievecollectionstask.cpp b/resources/imap/retrievecollectionstask.cpp
new file mode 100644 (file)
index 0000000..9173652
--- /dev/null
@@ -0,0 +1,235 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "retrievecollectionstask.h"
+
+#include "noselectattribute.h"
+#include "noinferiorsattribute.h"
+
+#include <cachepolicy.h>
+#include <entitydisplayattribute.h>
+#include <Akonadi/KMime/MessageParts>
+#include <AkonadiCore/VectorHelper>
+
+#include <kmime/kmime_message.h>
+
+#include "imapresource_debug.h"
+#include <KLocalizedString>
+
+RetrieveCollectionsTask::RetrieveCollectionsTask(ResourceStateInterface::Ptr resource, QObject *parent)
+    : ResourceTask(CancelIfNoSession, resource, parent)
+{
+}
+
+RetrieveCollectionsTask::~RetrieveCollectionsTask()
+{
+}
+
+void RetrieveCollectionsTask::doStart(KIMAP::Session *session)
+{
+    Akonadi::Collection root;
+    root.setName(resourceName());
+    root.setRemoteId(rootRemoteId());
+    root.setContentMimeTypes(QStringList(Akonadi::Collection::mimeType()));
+    root.setParentCollection(Akonadi::Collection::root());
+    root.addAttribute(new NoSelectAttribute(true));
+
+    Akonadi::CachePolicy policy;
+    policy.setInheritFromParent(false);
+    policy.setSyncOnDemand(true);
+
+    QStringList localParts;
+    localParts << QLatin1String(Akonadi::MessagePart::Envelope)
+               << QLatin1String(Akonadi::MessagePart::Header);
+    int cacheTimeout = 60;
+
+    if (isDisconnectedModeEnabled()) {
+        // For disconnected mode we also cache the body
+        // and we keep all data indifinitely
+        localParts << QLatin1String(Akonadi::MessagePart::Body);
+        cacheTimeout = -1;
+    }
+
+    policy.setLocalParts(localParts);
+    policy.setCacheTimeout(cacheTimeout);
+    policy.setIntervalCheckTime(intervalCheckTime());
+
+    root.setCachePolicy(policy);
+
+    m_reportedCollections.insert(QString(), root);
+
+    // this is ugly, but the result of LSUB is unfortunately not a sub-set of LIST
+    // it also contains subscribed but currently not available (eg. deleted) mailboxes
+    // so we need to use both and exclude mailboxes in LSUB but not in LIST
+    if (isSubscriptionEnabled()) {
+        KIMAP::ListJob *fullListJob = new KIMAP::ListJob(session);
+        fullListJob->setIncludeUnsubscribed(true);
+        fullListJob->setQueriedNamespaces(serverNamespaces());
+        connect(fullListJob, &KIMAP::ListJob::mailBoxesReceived,
+                this, &RetrieveCollectionsTask::onFullMailBoxesReceived);
+        connect(fullListJob, &KIMAP::ListJob::result, this, &RetrieveCollectionsTask::onFullMailBoxesReceiveDone);
+        fullListJob->start();
+    }
+
+    KIMAP::ListJob *listJob = new KIMAP::ListJob(session);
+    listJob->setIncludeUnsubscribed(!isSubscriptionEnabled());
+    listJob->setQueriedNamespaces(serverNamespaces());
+    connect(listJob, &KIMAP::ListJob::mailBoxesReceived,
+            this, &RetrieveCollectionsTask::onMailBoxesReceived);
+    connect(listJob, &KIMAP::ListJob::result, this, &RetrieveCollectionsTask::onMailBoxesReceiveDone);
+    listJob->start();
+}
+
+void RetrieveCollectionsTask::onMailBoxesReceived(const QList< KIMAP::MailBoxDescriptor > &descriptors,
+        const QList< QList<QByteArray> > &flags)
+{
+    QStringList contentTypes;
+    contentTypes << KMime::Message::mimeType() << Akonadi::Collection::mimeType();
+
+    if (!descriptors.isEmpty()) {
+        // This is still not optimal way of getting the separator, but it's better
+        // than guessing every time from RID of parent collection
+        setSeparatorCharacter(descriptors.first().separator);
+    }
+
+    for (int i = 0; i < descriptors.size(); ++i) {
+        KIMAP::MailBoxDescriptor descriptor = descriptors[i];
+
+        // skip phantom mailboxes contained in LSUB but not LIST
+        if (isSubscriptionEnabled() && !m_fullReportedCollections.contains(descriptor.name)) {
+            qCDebug(IMAPRESOURCE_LOG) << "Got phantom mailbox: " << descriptor.name;
+            continue;
+        }
+
+        const QString separator = descriptor.separator;
+        Q_ASSERT(separator.size() == 1);   // that's what the spec says
+
+        const QString boxName = descriptor.name.endsWith(separator)
+                                ? descriptor.name.left(descriptor.name.size() - 1)
+                                : descriptor.name;
+
+        const QStringList pathParts = boxName.split(separator);
+
+        QString parentPath;
+        QString currentPath;
+
+        for (int j = 0; j < pathParts.size(); ++j) {
+            const bool isDummy = j != pathParts.size() - 1;
+            const QString pathPart = pathParts.at(j);
+            currentPath += separator + pathPart;
+
+            if (m_reportedCollections.contains(currentPath)) {
+                if (m_dummyCollections.contains(currentPath) && !isDummy) {
+                    qCDebug(IMAPRESOURCE_LOG) << "Received the real collection for a dummy one : " << currentPath;
+
+                    //set the correct attributes for the collection, eg. noselect needs to be removed
+                    Akonadi::Collection c = m_reportedCollections.value(currentPath);
+                    c.setContentMimeTypes(contentTypes);
+                    c.setRights(Akonadi::Collection::AllRights);
+                    c.removeAttribute<NoSelectAttribute>();
+
+                    m_dummyCollections.remove(currentPath);
+                    m_reportedCollections.remove(currentPath);
+                    m_reportedCollections.insert(currentPath, c);
+
+                }
+                parentPath = currentPath;
+                continue;
+            }
+
+            const QList<QByteArray> currentFlags = isDummy ? (QList<QByteArray>() << "\\noselect") : flags[i];
+
+            Akonadi::Collection c;
+            c.setName(pathPart);
+            c.setRemoteId(separator + pathPart);
+            const Akonadi::Collection parentCollection = m_reportedCollections.value(parentPath);
+            c.setParentCollection(parentCollection);
+            c.setContentMimeTypes(contentTypes);
+
+            // If the folder is the Inbox, make some special settings.
+            if (currentPath.compare(separator + QLatin1String("INBOX"), Qt::CaseInsensitive) == 0) {
+                Akonadi::EntityDisplayAttribute *attr = c.attribute<Akonadi::EntityDisplayAttribute>(Akonadi::Collection::AddIfMissing);
+                attr->setDisplayName(i18n("Inbox"));
+                attr->setIconName(QStringLiteral("mail-folder-inbox"));
+                setIdleCollection(c);
+            }
+
+            // If the folder is the user top-level folder, mark it as well, even although it is not officially noted in the RFC
+            if (currentPath == (separator + QLatin1String("user")) && currentFlags.contains("\\noselect")) {
+                Akonadi::EntityDisplayAttribute *attr = c.attribute<Akonadi::EntityDisplayAttribute>(Akonadi::Collection::AddIfMissing);
+                attr->setDisplayName(i18n("Shared Folders"));
+                attr->setIconName(QStringLiteral("x-mail-distribution-list"));
+            }
+
+            // If this folder is a noselect folder, make some special settings.
+            if (currentFlags.contains("\\noselect")) {
+                qCDebug(IMAPRESOURCE_LOG) << "Dummy collection created: " << currentPath;
+                c.addAttribute(new NoSelectAttribute(true));
+                c.setContentMimeTypes(QStringList() << Akonadi::Collection::mimeType());
+                c.setRights(Akonadi::Collection::ReadOnly);
+            } else {
+                // remove the noselect attribute explicitly, in case we had set it before (eg. for non-subscribed non-leaf folders)
+                c.removeAttribute<NoSelectAttribute>();
+            }
+
+            // If this folder is a noinferiors folder, it is not allowed to create subfolders inside.
+            if (currentFlags.contains("\\noinferiors")) {
+                //qCDebug(IMAPRESOURCE_LOG) << "Noinferiors: " << currentPath;
+                c.addAttribute(new NoInferiorsAttribute(true));
+                c.setRights(c.rights() & ~Akonadi::Collection::CanCreateCollection);
+            }
+
+            m_reportedCollections.insert(currentPath, c);
+
+            if (isDummy) {
+                m_dummyCollections.insert(currentPath, c);
+            }
+
+            parentPath = currentPath;
+        }
+    }
+}
+
+void RetrieveCollectionsTask::onMailBoxesReceiveDone(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(job->errorString());
+    } else {
+        collectionsRetrieved(Akonadi::valuesToVector(m_reportedCollections));
+    }
+}
+
+void RetrieveCollectionsTask::onFullMailBoxesReceived(const QList< KIMAP::MailBoxDescriptor > &descriptors,
+        const QList< QList< QByteArray > > &flags)
+{
+    Q_UNUSED(flags);
+    foreach (const KIMAP::MailBoxDescriptor &descriptor, descriptors) {
+        m_fullReportedCollections.insert(descriptor.name);
+    }
+}
+
+void RetrieveCollectionsTask::onFullMailBoxesReceiveDone(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(job->errorString());
+    }
+}
+
diff --git a/resources/imap/retrievecollectionstask.h b/resources/imap/retrievecollectionstask.h
new file mode 100644 (file)
index 0000000..3f2299a
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef RETRIEVECOLLECTIONSTASK_H
+#define RETRIEVECOLLECTIONSTASK_H
+
+#include <collection.h>
+
+#include <kimap/listjob.h>
+
+#include "resourcetask.h"
+
+class RetrieveCollectionsTask : public ResourceTask
+{
+    Q_OBJECT
+
+public:
+    explicit RetrieveCollectionsTask(ResourceStateInterface::Ptr resource, QObject *parent = Q_NULLPTR);
+    virtual ~RetrieveCollectionsTask();
+
+private Q_SLOTS:
+    void onMailBoxesReceived(const QList<KIMAP::MailBoxDescriptor> &descriptors,
+                             const QList< QList<QByteArray> > &flags);
+    void onMailBoxesReceiveDone(KJob *job);
+    void onFullMailBoxesReceived(const QList<KIMAP::MailBoxDescriptor> &descriptors, const QList<QList<QByteArray> > &flags);
+    void onFullMailBoxesReceiveDone(KJob *job);
+
+protected:
+    void doStart(KIMAP::Session *session) Q_DECL_OVERRIDE;
+
+protected:
+    QHash<QString, Akonadi::Collection> m_reportedCollections;
+    QHash<QString, Akonadi::Collection> m_dummyCollections;
+    QSet<QString> m_fullReportedCollections;
+};
+
+#endif
diff --git a/resources/imap/retrieveitemstask.cpp b/resources/imap/retrieveitemstask.cpp
new file mode 100644 (file)
index 0000000..783d753
--- /dev/null
@@ -0,0 +1,646 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "retrieveitemstask.h"
+
+#include "collectionflagsattribute.h"
+#include "noselectattribute.h"
+#include "uidvalidityattribute.h"
+#include "uidnextattribute.h"
+#include "highestmodseqattribute.h"
+#include "messagehelper.h"
+#include "batchfetcher.h"
+
+#include <AkonadiCore/cachepolicy.h>
+#include <AkonadiCore/collectionstatistics.h>
+#include <Akonadi/KMime/MessageParts>
+#include <AkonadiAgentBase/agentbase.h>
+#include <AkonadiCore/itemfetchjob.h>
+#include <AkonadiCore/itemfetchscope.h>
+#include <AkonadiCore/session.h>
+
+#include "imapresource_debug.h"
+#include "imapresource_debug.h"
+
+#include "imapresource_debug.h"
+#include "imapresource_debug.h"
+
+#include <KLocalizedString>
+
+#include <kimap/expungejob.h>
+#include <kimap/fetchjob.h>
+#include <kimap/selectjob.h>
+#include <kimap/session.h>
+#include <kimap/searchjob.h>
+#include <kimap/statusjob.h>
+
+RetrieveItemsTask::RetrieveItemsTask(ResourceStateInterface::Ptr resource, QObject *parent)
+    : ResourceTask(CancelIfNoSession, resource, parent),
+      m_session(Q_NULLPTR),
+      m_fetchedMissingBodies(-1),
+      m_fetchMissingBodies(false),
+      m_incremental(true),
+      m_localHighestModSeq(-1),
+      m_batchFetcher(Q_NULLPTR),
+      m_uidBasedFetch(true),
+      m_flagsChanged(false),
+      m_messageCount(-1),
+      m_uidValidity(-1),
+      m_nextUid(-1),
+      m_highestModSeq(-1)
+{
+
+}
+
+RetrieveItemsTask::~RetrieveItemsTask()
+{
+}
+
+void RetrieveItemsTask::setFetchMissingItemBodies(bool enabled)
+{
+    m_fetchMissingBodies = enabled;
+}
+
+void RetrieveItemsTask::doStart(KIMAP::Session *session)
+{
+    emitPercent(0);
+    // Prevent fetching items from noselect folders.
+    if (collection().hasAttribute("noselect")) {
+        NoSelectAttribute *noselect = static_cast<NoSelectAttribute *>(collection().attribute("noselect"));
+        if (noselect->noSelect()) {
+            qCDebug(IMAPRESOURCE_LOG) << "No Select folder";
+            itemsRetrievalDone();
+            return;
+        }
+    }
+
+    m_session = session;
+
+    const Akonadi::Collection col = collection();
+    // Only with emails we can be sure that RID is persistent and thus we can use
+    // it for merging. For other potential content types (like Kolab events etc.)
+    // use GID instead.
+    QStringList cts = col.contentMimeTypes();
+    cts.removeOne(Akonadi::Collection::mimeType());
+    cts.removeOne(KMime::Message::mimeType());
+    if (!cts.isEmpty()) {
+        setItemMergingMode(Akonadi::ItemSync::GIDMerge);
+    }
+
+    if (m_fetchMissingBodies && col.cachePolicy()
+            .localParts().contains(QLatin1String(Akonadi::MessagePart::Body))) {  //disconnected mode, make sure we really have the body cached
+
+        Akonadi::Session *session = new Akonadi::Session(resourceName().toLatin1() + "_body_checker", this);
+        Akonadi::ItemFetchJob *fetchJob = new Akonadi::ItemFetchJob(col, session);
+        fetchJob->fetchScope().setCheckForCachedPayloadPartsOnly();
+        fetchJob->fetchScope().fetchPayloadPart(Akonadi::MessagePart::Body);
+        fetchJob->fetchScope().setFetchModificationTime(false);
+        connect(fetchJob, &Akonadi::ItemFetchJob::result, this, &RetrieveItemsTask::fetchItemsWithoutBodiesDone);
+        connect(fetchJob, &Akonadi::ItemFetchJob::result, session, &Akonadi::Session::deleteLater);
+    } else {
+        startRetrievalTasks();
+    }
+}
+
+BatchFetcher *RetrieveItemsTask::createBatchFetcher(MessageHelper::Ptr messageHelper,
+        const KIMAP::ImapSet &set,
+        const KIMAP::FetchJob::FetchScope &scope,
+        int batchSize, KIMAP::Session *session)
+{
+    return new BatchFetcher(messageHelper, set, scope, batchSize, session);
+}
+
+void RetrieveItemsTask::fetchItemsWithoutBodiesDone(KJob *job)
+{
+    QVector<qint64> uids;
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << job->errorString();
+        cancelTask(job->errorString());
+        return;
+    } else {
+        int i = 0;
+        Akonadi::ItemFetchJob *fetch = static_cast<Akonadi::ItemFetchJob *>(job);
+        Q_FOREACH (const Akonadi::Item &item, fetch->items())  {
+            if (!item.cachedPayloadParts().contains(Akonadi::MessagePart::Body)) {
+                qCWarning(IMAPRESOURCE_LOG) << "Item " << item.id() << " is missing the payload! Cached payloads: " << item.cachedPayloadParts();
+                uids.append(item.remoteId().toInt());
+                i++;
+            }
+        }
+        if (i > 0) {
+            qCWarning(IMAPRESOURCE_LOG) << "Number of items missing the body: " << i;
+        }
+    }
+    onFetchItemsWithoutBodiesDone(uids);
+}
+
+void RetrieveItemsTask::onFetchItemsWithoutBodiesDone(const QVector<qint64> &items)
+{
+    m_messageUidsMissingBody = items;
+    startRetrievalTasks();
+}
+
+void RetrieveItemsTask::startRetrievalTasks()
+{
+    const QString mailBox = mailBoxForCollection(collection());
+    qCDebug(IMAPRESOURCE_LOG) << "Starting retrieval for " << mailBox;
+    m_time.start();
+
+    // Now is the right time to expunge the messages marked \\Deleted from this mailbox.
+    const bool hasACL = serverCapabilities().contains(QStringLiteral("ACL"));
+    const KIMAP::Acl::Rights rights = myRights(collection());
+    if (isAutomaticExpungeEnabled() && (!hasACL || (rights & KIMAP::Acl::Expunge) || (rights & KIMAP::Acl::Delete))) {
+        if (m_session->selectedMailBox() != mailBox) {
+            triggerPreExpungeSelect(mailBox);
+        } else {
+            triggerExpunge(mailBox);
+        }
+    } else {
+        // Always select to get the stats updated
+        triggerFinalSelect(mailBox);
+    }
+}
+
+void RetrieveItemsTask::triggerPreExpungeSelect(const QString &mailBox)
+{
+    KIMAP::SelectJob *select = new KIMAP::SelectJob(m_session);
+    select->setMailBox(mailBox);
+    select->setCondstoreEnabled(serverSupportsCondstore());
+    connect(select, &KJob::result,
+            this, &RetrieveItemsTask::onPreExpungeSelectDone);
+    select->start();
+}
+
+void RetrieveItemsTask::onPreExpungeSelectDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << job->errorString();
+        cancelTask(job->errorString());
+    } else {
+        KIMAP::SelectJob *select = static_cast<KIMAP::SelectJob *>(job);
+        triggerExpunge(select->mailBox());
+    }
+}
+
+void RetrieveItemsTask::triggerExpunge(const QString &mailBox)
+{
+    KIMAP::ExpungeJob *expunge = new KIMAP::ExpungeJob(m_session);
+    connect(expunge, &KJob::result,
+            this, &RetrieveItemsTask::onExpungeDone);
+    expunge->start();
+}
+
+void RetrieveItemsTask::onExpungeDone(KJob *job)
+{
+    // We can ignore the error, we just had a wrong expunge so some old messages will just reappear.
+    // TODO we should probably hide messages that are marked as deleted (skipping will not work because we rely on the message count)
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Expunge failed: " << job->errorString();
+    }
+    // Except for network errors.
+    if (job->error() && m_session->state() == KIMAP::Session::Disconnected) {
+        cancelTask(job->errorString());
+        return;
+    }
+
+    // We have to re-select the mailbox to update all the stats after the expunge
+    // (the EXPUNGE command doesn't return enough for our needs)
+    triggerFinalSelect(m_session->selectedMailBox());
+}
+
+void RetrieveItemsTask::triggerFinalSelect(const QString &mailBox)
+{
+    KIMAP::SelectJob *select = new KIMAP::SelectJob(m_session);
+    select->setMailBox(mailBox);
+    select->setCondstoreEnabled(serverSupportsCondstore());
+    connect(select, &KJob::result,
+            this, &RetrieveItemsTask::onFinalSelectDone);
+    select->start();
+}
+
+void RetrieveItemsTask::onFinalSelectDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << job->errorString();
+        cancelTask(job->errorString());
+        return;
+    }
+
+    KIMAP::SelectJob *select = qobject_cast<KIMAP::SelectJob *>(job);
+
+    m_mailBox = select->mailBox();
+    m_messageCount = select->messageCount();
+    m_uidValidity = select->uidValidity();
+    m_nextUid = select->nextUid();
+    m_highestModSeq = select->highestModSequence();
+    m_flags = select->permanentFlags();
+
+    //This is known to happen with Courier IMAP.
+    if (m_nextUid < 0) {
+        KIMAP::StatusJob *status = new KIMAP::StatusJob(m_session);
+        status->setMailBox(m_mailBox);
+        status->setDataItems({ "UIDNEXT" });
+        connect(status, &KJob::result,
+                this, &RetrieveItemsTask::onStatusDone);
+        status->start();
+    } else {
+        prepareRetrieval();
+    }
+}
+
+void RetrieveItemsTask::onStatusDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << job->errorString();
+        cancelTask(job->errorString());
+        return;
+    }
+
+    KIMAP::StatusJob *status = qobject_cast<KIMAP::StatusJob*>(job);
+    QList<QPair<QByteArray, qint64>> results = status->status();
+    Q_FOREACH (const auto &val, results) {
+        if (val.first == "UIDNEXT") {
+            m_nextUid = val.second;
+            break;
+        }
+    }
+
+    prepareRetrieval();
+}
+
+void RetrieveItemsTask::prepareRetrieval()
+{
+
+    // Handle invalid UIDNEXT in case even STATUS is not able to retrieve it
+    if (m_nextUid < 0) {
+        qCWarning(IMAPRESOURCE_LOG) << "Server bug: Your IMAP Server delivered an invalid UIDNEXT value.";
+        m_nextUid = 0;
+    }
+
+    //The select job retrieves highestmodseq whenever it's available, but in case of no CONDSTORE support we ignore it
+    if (!serverSupportsCondstore()) {
+        m_localHighestModSeq = 0;
+    }
+
+    Akonadi::Collection col = collection();
+    bool modifyNeeded = false;
+
+    // Get the current uid validity value and store it
+    int oldUidValidity = 0;
+    if (!col.hasAttribute("uidvalidity")) {
+        UidValidityAttribute *currentUidValidity  = new UidValidityAttribute(m_uidValidity);
+        col.addAttribute(currentUidValidity);
+        modifyNeeded = true;
+    } else {
+        UidValidityAttribute *currentUidValidity =
+            static_cast<UidValidityAttribute *>(col.attribute("uidvalidity"));
+        oldUidValidity = currentUidValidity->uidValidity();
+        if (oldUidValidity != m_uidValidity) {
+            currentUidValidity->setUidValidity(m_uidValidity);
+            modifyNeeded = true;
+        }
+    }
+
+    // Get the current uid next value and store it
+    int oldNextUid = 0;
+    if (m_nextUid > 0) { //this can fail with faulty servers that don't deliver uidnext
+        if (UidNextAttribute *currentNextUid = col.attribute<UidNextAttribute>()) {
+            oldNextUid = currentNextUid->uidNext();
+            if (oldNextUid != m_nextUid) {
+                currentNextUid->setUidNext(m_nextUid);
+                modifyNeeded = true;
+            }
+        } else {
+            col.attribute<UidNextAttribute>(Akonadi::Collection::AddIfMissing)->setUidNext(m_nextUid);
+            modifyNeeded = true;
+        }
+    }
+
+    // Store the mailbox flags
+    if (!col.hasAttribute("collectionflags")) {
+        Akonadi::CollectionFlagsAttribute *flagsAttribute  = new Akonadi::CollectionFlagsAttribute(m_flags);
+        col.addAttribute(flagsAttribute);
+        modifyNeeded = true;
+    } else {
+        Akonadi::CollectionFlagsAttribute *flagsAttribute =
+            static_cast<Akonadi::CollectionFlagsAttribute *>(col.attribute("collectionflags"));
+        const QList<QByteArray> oldFlags = flagsAttribute->flags();
+        if (oldFlags != m_flags) {
+            flagsAttribute->setFlags(m_flags);
+            modifyNeeded = true;
+        }
+    }
+
+    qint64 oldHighestModSeq = 0;
+    if (serverSupportsCondstore() && m_highestModSeq > 0) {
+        if (!col.hasAttribute("highestmodseq")) {
+            HighestModSeqAttribute *attr = new HighestModSeqAttribute(m_highestModSeq);
+            col.addAttribute(attr);
+            modifyNeeded = true;
+        } else {
+            HighestModSeqAttribute *attr = col.attribute<HighestModSeqAttribute>();
+            if (attr->highestModSequence() < m_highestModSeq) {
+                oldHighestModSeq = attr->highestModSequence();
+                attr->setHighestModSeq(m_highestModSeq);
+                modifyNeeded = true;
+            } else if (attr->highestModSequence() == m_highestModSeq) {
+                oldHighestModSeq = attr->highestModSequence();
+            } else if (attr->highestModSequence() > m_highestModSeq) {
+                // This situation should not happen. If it does, update the highestModSeq
+                // attribute, but rather do a full sync
+                attr->setHighestModSeq(m_highestModSeq);
+                modifyNeeded = true;
+            }
+        }
+    }
+    m_localHighestModSeq = oldHighestModSeq;
+
+    if (modifyNeeded) {
+        m_modifiedCollection = col;
+    }
+
+    KIMAP::FetchJob::FetchScope scope;
+    scope.parts.clear();
+    scope.mode = KIMAP::FetchJob::FetchScope::FullHeaders;
+
+    if (col.cachePolicy()
+            .localParts().contains(QLatin1String(Akonadi::MessagePart::Body))) {
+        scope.mode = KIMAP::FetchJob::FetchScope::Full;
+    }
+
+    const qint64 realMessageCount = col.statistics().count();
+
+    qCDebug(IMAPRESOURCE_LOG) << "Starting message retrieval. Elapsed(ms): " << m_time.elapsed();
+    qCDebug(IMAPRESOURCE_LOG) << "MessageCount: " << m_messageCount << "Local message count: " << realMessageCount;
+    qCDebug(IMAPRESOURCE_LOG) << "UidNext: " << m_nextUid << "Local UidNext: " << oldNextUid;
+    qCDebug(IMAPRESOURCE_LOG) << "HighestModSeq: " << m_highestModSeq << "Local HighestModSeq: " << oldHighestModSeq;
+
+    /*
+    * A synchronization has 3 mandatory steps:
+    * * If uidvalidity changed the local cache must be invalidated
+    * * New messages can be fetched usin uidNext and the last known fetched uid
+    * * flag changes and removals can be detected by listing all messages that weren't part of the previous step
+    *
+    * Everything else is optimizations.
+    *
+    * TODO: Note that the local message count can be larger than the remote message count although no messages
+    * have been deleted remotely, if we locally have messages that were not yet uploaded.
+    * We cannot differentiate that from remotely removed messages, so we have to do a full flag
+    * listing in that case. This can be optimized once we support QRESYNC and therefore have a way
+    * to determine whether messages have been removed.
+    */
+
+    if (m_messageCount == 0) {
+        //Shortcut:
+        //If no messages are present on the server, clear local cash and finish
+        m_incremental = false;
+        if (realMessageCount > 0) {
+            qCDebug(IMAPRESOURCE_LOG) << "No messages present so we are done, deleting local messages.";
+            itemsRetrieved(Akonadi::Item::List());
+        } else {
+            qCDebug(IMAPRESOURCE_LOG) << "No messages present so we are done";
+        }
+        taskComplete();
+    } else if (oldUidValidity != m_uidValidity || m_nextUid <= 0) {
+        //If uidvalidity has changed our local cache is worthless and has to be refetched completely
+        if (oldUidValidity != 0 && oldUidValidity != m_uidValidity) {
+            qCDebug(IMAPRESOURCE_LOG) << "UIDVALIDITY check failed (" << oldUidValidity << "|" << m_uidValidity << ")";
+        }
+        if (m_nextUid <= 0) {
+            qCDebug(IMAPRESOURCE_LOG) << "Invalid UIDNEXT";
+        }
+        qCDebug(IMAPRESOURCE_LOG) << "Fetching complete mailbox " << m_mailBox;
+        setTotalItems(m_messageCount);
+        retrieveItems(KIMAP::ImapSet(1, m_nextUid), scope, false, true);
+    } else if (m_nextUid <= 0) {
+        //This is a compatibilty codepath for Courier IMAP. It probably introduces problems, but at least it syncs.
+        //Since we don't have uidnext available, we simply use the messagecount. This will miss simultaneously added/removed messages.
+        //qCDebug(IMAPRESOURCE_LOG) << "Running courier imap compatibility codepath";
+        if (m_messageCount > realMessageCount) {
+            //Get new messages
+            retrieveItems(KIMAP::ImapSet(realMessageCount + 1, m_messageCount), scope, false, false);
+        } else if (m_messageCount == realMessageCount) {
+            m_uidBasedFetch = false;
+            m_incremental = true;
+            setTotalItems(m_messageCount);
+            listFlagsForImapSet(KIMAP::ImapSet(1, m_messageCount));
+        } else {
+            m_uidBasedFetch = false;
+            m_incremental = false;
+            setTotalItems(m_messageCount);
+            listFlagsForImapSet(KIMAP::ImapSet(1, m_messageCount));
+        }
+    } else if (!m_messageUidsMissingBody.isEmpty()) {
+        //fetch missing uids
+        m_fetchedMissingBodies = 0;
+        setTotalItems(m_messageUidsMissingBody.size());
+        KIMAP::ImapSet imapSet;
+        imapSet.add(m_messageUidsMissingBody);
+        retrieveItems(imapSet, scope, true, true);
+    } else if (m_nextUid > oldNextUid && ((realMessageCount + m_nextUid - oldNextUid) == m_messageCount) && realMessageCount > 0) {
+        //Optimization:
+        //New messages are available, but we know no messages have been removed.
+        //Fetch new messages, and then check for changed flags and removed messages
+        //We can make an incremental update and use modseq.
+        qCDebug(IMAPRESOURCE_LOG) << "Incrementally fetching new messages: UidNext: " << m_nextUid << " Old UidNext: " << oldNextUid << " message count " << m_messageCount << realMessageCount;
+        setTotalItems(qMax(1ll, m_messageCount - realMessageCount));
+        m_flagsChanged = !(m_highestModSeq == oldHighestModSeq);
+        retrieveItems(KIMAP::ImapSet(qMax(1, oldNextUid), m_nextUid), scope, true, true);
+    } else if (m_nextUid > oldNextUid && m_messageCount > (realMessageCount + m_nextUid - oldNextUid) && realMessageCount > 0) {
+        //Error recovery:
+        //New messages are available, but not enough to justify the difference between the local and remote message count.
+        //This can be triggered if we i.e. clear the local cache, but the keep the annotations.
+        //If we didn't catch this case, we end up inserting flags only for every missing message.
+        qCWarning(IMAPRESOURCE_LOG) << "Detected inconsistency in local cache, we're missing some messages. Server: " << m_messageCount << " Local: " << realMessageCount;
+        qCWarning(IMAPRESOURCE_LOG) << "Refetching complete mailbox.";
+        setTotalItems(m_messageCount);
+        retrieveItems(KIMAP::ImapSet(1, m_nextUid), scope, false, true);
+    } else if (m_nextUid > oldNextUid) {
+        //New messages are available. Fetch new messages, and then check for changed flags and removed messages
+        qCDebug(IMAPRESOURCE_LOG) << "Fetching new messages: UidNext: " << m_nextUid << " Old UidNext: " << oldNextUid;
+        setTotalItems(m_messageCount);
+        retrieveItems(KIMAP::ImapSet(qMax(1, oldNextUid), m_nextUid), scope, false, true);
+    } else if (m_messageCount == realMessageCount && oldNextUid == m_nextUid) {
+        //Optimization:
+        //We know no messages were added or removed (if the message count and uidnext is still the same)
+        //We only check the flags incrementally and can make use of modseq
+        m_uidBasedFetch = true;
+        m_incremental = true;
+        m_flagsChanged = !(m_highestModSeq == oldHighestModSeq);
+        //Workaround: If the server doesn't support CONDSTORE we would end up syncing all flags during every sync.
+        //Instead we only sync flags when new messages are available or removed and skip this step.
+        //WARNING: This sacrifices consistency as we will not detect flag changes until a new message enters the mailbox.
+        if (m_incremental && !serverSupportsCondstore()) {
+            qCDebug(IMAPRESOURCE_LOG) << "Avoiding flag sync due to missing CONDSTORE support";
+            taskComplete();
+            return;
+        }
+        setTotalItems(m_messageCount);
+        listFlagsForImapSet(KIMAP::ImapSet(1, m_nextUid));
+    } else if (m_messageCount > realMessageCount) {
+        //Error recovery:
+        //We didn't detect any new messages based on the uid, but according to the message count there are new ones.
+        //Our local cache is invalid and has to be refetched.
+        qCWarning(IMAPRESOURCE_LOG) << "Detected inconsistency in local cache, we're missing some messages. Server: " << m_messageCount << " Local: " << realMessageCount;
+        qCWarning(IMAPRESOURCE_LOG) << "Refetching complete mailbox.";
+        setTotalItems(m_messageCount);
+        retrieveItems(KIMAP::ImapSet(1, m_nextUid), scope, false, true);
+    } else {
+        //Shortcut:
+        //No new messages are available. Directly check for changed flags and removed messages.
+        m_uidBasedFetch = true;
+        m_incremental = false;
+        setTotalItems(m_messageCount);
+        listFlagsForImapSet(KIMAP::ImapSet(1, m_nextUid));
+    }
+}
+
+void RetrieveItemsTask::retrieveItems(const KIMAP::ImapSet &set, const KIMAP::FetchJob::FetchScope &scope, bool incremental, bool uidBased)
+{
+    Q_ASSERT(set.intervals().size() == 1);
+
+    m_incremental = incremental;
+    m_uidBasedFetch = uidBased;
+
+    m_batchFetcher = createBatchFetcher(resourceState()->messageHelper(), set, scope, batchSize(), m_session);
+    m_batchFetcher->setUidBased(m_uidBasedFetch);
+    if (m_uidBasedFetch && set.intervals().size() == 1) {
+        m_batchFetcher->setSearchUids(set.intervals().front());
+    }
+    m_batchFetcher->setProperty("alreadyFetched", set.intervals().at(0).begin());
+    connect(m_batchFetcher, &BatchFetcher::itemsRetrieved,
+            this, &RetrieveItemsTask::onItemsRetrieved);
+    connect(m_batchFetcher, &KJob::result,
+            this, &RetrieveItemsTask::onRetrievalDone);
+    m_batchFetcher->start();
+}
+
+void RetrieveItemsTask::onReadyForNextBatch(int size)
+{
+    Q_UNUSED(size);
+    if (m_batchFetcher) {
+        m_batchFetcher->fetchNextBatch();
+    }
+}
+
+void RetrieveItemsTask::onItemsRetrieved(const Akonadi::Item::List &addedItems)
+{
+    if (m_incremental) {
+        itemsRetrievedIncremental(addedItems, Akonadi::Item::List());
+    } else {
+        itemsRetrieved(addedItems);
+    }
+
+    //m_fetchedMissingBodies is -1 if we fetch for other reason, but missing bodies
+    if (m_fetchedMissingBodies != -1) {
+        const QString mailBox = mailBoxForCollection(collection());
+        m_fetchedMissingBodies += addedItems.count();
+        Q_EMIT status(Akonadi::AgentBase::Running,
+                      i18nc("@info:status", "Fetching missing mail bodies in %3: %1/%2", m_fetchedMissingBodies, m_messageUidsMissingBody.count(), mailBox));
+    }
+}
+
+void RetrieveItemsTask::onRetrievalDone(KJob *job)
+{
+    m_batchFetcher = Q_NULLPTR;
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << job->errorString();
+        cancelTask(job->errorString());
+        m_fetchedMissingBodies = -1;
+        return;
+    }
+
+    //This is the lowest sequence number that we just fetched.
+    const KIMAP::ImapSet::Id alreadyFetchedBegin = job->property("alreadyFetched").value<KIMAP::ImapSet::Id>();
+
+    // If this is the first fetch of a folder, skip getting flags, we
+    // already have them all from the previous full fetch. This is not
+    // just an optimization, as incremental retrieval assumes nothing
+    // will be listed twice.
+    if (m_fetchedMissingBodies != -1 || alreadyFetchedBegin <= 1) {
+        taskComplete();
+        return;
+    }
+
+    // Fetch flags of all items that were not fetched by the fetchJob. After
+    // that /all/ items in the folder are synced.
+    listFlagsForImapSet(KIMAP::ImapSet(1, alreadyFetchedBegin - 1));
+}
+
+void RetrieveItemsTask::listFlagsForImapSet(const KIMAP::ImapSet &set)
+{
+    qCDebug(IMAPRESOURCE_LOG) << "Listing flags " << set.intervals().at(0).begin() << set.intervals().at(0).end();
+    qCDebug(IMAPRESOURCE_LOG) << "Starting flag retrieval. Elapsed(ms): " << m_time.elapsed();
+
+    KIMAP::FetchJob::FetchScope scope;
+    scope.parts.clear();
+    scope.mode = KIMAP::FetchJob::FetchScope::Flags;
+    // Only use changeSince when doing incremental listings,
+    // otherwise we would overwrite our local data with an incomplete dataset
+    if (m_incremental && serverSupportsCondstore()) {
+        scope.changedSince = m_localHighestModSeq;
+        if (!m_flagsChanged) {
+            qCDebug(IMAPRESOURCE_LOG)  << "No flag changes.";
+            taskComplete();
+            return;
+        }
+    }
+
+    m_batchFetcher = createBatchFetcher(resourceState()->messageHelper(), set, scope, 10 * batchSize(), m_session);
+    m_batchFetcher->setUidBased(m_uidBasedFetch);
+    if (m_uidBasedFetch && scope.changedSince == 0 && set.intervals().size() == 1) {
+        m_batchFetcher->setSearchUids(set.intervals().front());
+    }
+    connect(m_batchFetcher, &BatchFetcher::itemsRetrieved,
+            this, &RetrieveItemsTask::onItemsRetrieved);
+    connect(m_batchFetcher, &KJob::result,
+            this, &RetrieveItemsTask::onFlagsFetchDone);
+    m_batchFetcher->start();
+}
+
+void RetrieveItemsTask::onFlagsFetchDone(KJob *job)
+{
+    m_batchFetcher = Q_NULLPTR;
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << job->errorString();
+        cancelTask(job->errorString());
+    } else {
+        taskComplete();
+    }
+}
+
+void RetrieveItemsTask::taskComplete()
+{
+    if (m_modifiedCollection.isValid()) {
+        qCDebug(IMAPRESOURCE_LOG) << "Applying collection changes";
+        applyCollectionChanges(m_modifiedCollection);
+    }
+    if (m_incremental) {
+        // Calling itemsRetrievalDone() before previous call to itemsRetrievedIncremental()
+        // behaves like if we called itemsRetrieved(Items::List()), so make sure
+        // Akonadi knows we did incremental fetch that came up with no changes
+        itemsRetrievedIncremental(Akonadi::Item::List(), Akonadi::Item::List());
+    }
+    qCDebug(IMAPRESOURCE_LOG) << "Retrieval complete. Elapsed(ms): " << m_time.elapsed();
+    itemsRetrievalDone();
+}
+
diff --git a/resources/imap/retrieveitemstask.h b/resources/imap/retrieveitemstask.h
new file mode 100644 (file)
index 0000000..897d460
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef RETRIEVEITEMSTASK_H
+#define RETRIEVEITEMSTASK_H
+
+#include <kimap/fetchjob.h>
+
+#include "resourcetask.h"
+
+class BatchFetcher;
+namespace Akonadi
+{
+class Session;
+}
+
+class RetrieveItemsTask : public ResourceTask
+{
+    Q_OBJECT
+
+public:
+    explicit RetrieveItemsTask(ResourceStateInterface::Ptr resource, QObject *parent = Q_NULLPTR);
+    virtual ~RetrieveItemsTask();
+    void setFetchMissingItemBodies(bool enabled);
+
+public Q_SLOTS:
+    void onFetchItemsWithoutBodiesDone(const QVector<qint64> &items);
+    void onReadyForNextBatch(int size);
+
+private Q_SLOTS:
+    void fetchItemsWithoutBodiesDone(KJob *job);
+    void onPreExpungeSelectDone(KJob *job);
+    void onExpungeDone(KJob *job);
+    void onFinalSelectDone(KJob *job);
+    void onStatusDone(KJob *job);
+    void onItemsRetrieved(const Akonadi::Item::List &addedItems);
+    void onRetrievalDone(KJob *job);
+    void onFlagsFetchDone(KJob *job);
+
+protected:
+    void doStart(KIMAP::Session *session) Q_DECL_OVERRIDE;
+
+    virtual BatchFetcher *createBatchFetcher(MessageHelper::Ptr messageHelper, const KIMAP::ImapSet &set,
+            const KIMAP::FetchJob::FetchScope &scope, int batchSize,
+            KIMAP::Session *session);
+
+private:
+    void prepareRetrieval();
+    void startRetrievalTasks();
+    void triggerPreExpungeSelect(const QString &mailBox);
+    void triggerExpunge(const QString &mailBox);
+    void triggerFinalSelect(const QString &mailBox);
+    void retrieveItems(const KIMAP::ImapSet &set, const KIMAP::FetchJob::FetchScope &scope, bool incremental = false, bool uidBased = false);
+    void listFlagsForImapSet(const KIMAP::ImapSet &set);
+    void taskComplete();
+
+    KIMAP::Session *m_session;
+    QVector<qint64> m_messageUidsMissingBody;
+    int m_fetchedMissingBodies;
+    bool m_fetchMissingBodies;
+    bool m_incremental;
+    qint64 m_localHighestModSeq;
+    BatchFetcher *m_batchFetcher;
+    Akonadi::Collection m_modifiedCollection;
+    bool m_uidBasedFetch;
+    bool m_flagsChanged;
+    QTime m_time;
+
+    // Results of SELECT
+    QString m_mailBox;
+    int m_messageCount;
+    int m_uidValidity;
+    qint64 m_nextUid;
+    qint64 m_highestModSeq;
+    QList<QByteArray> m_flags;
+};
+
+#endif
diff --git a/resources/imap/retrieveitemtask.cpp b/resources/imap/retrieveitemtask.cpp
new file mode 100644 (file)
index 0000000..fadb273
--- /dev/null
@@ -0,0 +1,147 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "retrieveitemtask.h"
+#include "messagehelper.h"
+
+#include "imapresource_debug.h"
+#include "imapresource_debug.h"
+
+#include <KLocalizedString>
+
+#include <Akonadi/KMime/MessageFlags>
+#include <kimap/selectjob.h>
+#include <kimap/session.h>
+
+RetrieveItemTask::RetrieveItemTask(ResourceStateInterface::Ptr resource, QObject *parent)
+    : ResourceTask(CancelIfNoSession, resource, parent), m_session(Q_NULLPTR), m_uid(0), m_messageReceived(false)
+{
+
+}
+
+RetrieveItemTask::~RetrieveItemTask()
+{
+}
+
+void RetrieveItemTask::doStart(KIMAP::Session *session)
+{
+    m_session = session;
+
+    const QString mailBox = mailBoxForCollection(item().parentCollection());
+    m_uid = item().remoteId().toLongLong();
+
+    if (m_uid == 0) {
+        qCWarning(IMAPRESOURCE_LOG) << "Remote id is " << item().remoteId();
+        cancelTask(i18n("Remote id is empty or invalid"));
+        return;
+    }
+
+    if (session->selectedMailBox() != mailBox) {
+        KIMAP::SelectJob *select = new KIMAP::SelectJob(m_session);
+        select->setMailBox(mailBox);
+        connect(select, &KJob::result,
+                this, &RetrieveItemTask::onSelectDone);
+        select->start();
+    } else {
+        triggerFetchJob();
+    }
+}
+
+void RetrieveItemTask::onSelectDone(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(job->errorString());
+    } else {
+        triggerFetchJob();
+    }
+}
+
+void RetrieveItemTask::triggerFetchJob()
+{
+    KIMAP::FetchJob *fetch = new KIMAP::FetchJob(m_session);
+    KIMAP::FetchJob::FetchScope scope;
+    fetch->setUidBased(true);
+    fetch->setSequenceSet(KIMAP::ImapSet(m_uid));
+    scope.parts.clear();// = parts.toList();
+    scope.mode = KIMAP::FetchJob::FetchScope::Content;
+    fetch->setScope(scope);
+    connect(fetch, SIGNAL(messagesReceived(QString,
+                                           QMap<qint64, qint64>,
+                                           QMap<qint64, KIMAP::MessageAttribute>,
+                                           QMap<qint64, KIMAP::MessagePtr>)),
+            this, SLOT(onMessagesReceived(QString,
+                                          QMap<qint64, qint64>,
+                                          QMap<qint64, KIMAP::MessageAttribute>,
+                                          QMap<qint64, KIMAP::MessagePtr>)));
+    //TODO: Handle parts retrieval
+    //connect( fetch, SIGNAL(partsReceived(QString,QMap<qint64,qint64>,QMap<qint64,KIMAP::MessageParts>)),
+    //         this, SLOT(onPartsReceived(QString,QMap<qint64,qint64>,QMap<qint64,KIMAP::MessageParts>)) );
+    connect(fetch, &KJob::result,
+            this, &RetrieveItemTask::onContentFetchDone);
+    fetch->start();
+}
+
+void RetrieveItemTask::onMessagesReceived(const QString &mailBox,
+        const QMap<qint64, qint64> &uids,
+        const QMap<qint64, KIMAP::MessageAttribute> &attrs,
+        const QMap<qint64, KIMAP::MessagePtr> &messages)
+{
+    Q_UNUSED(mailBox);
+
+    KIMAP::FetchJob *fetch = qobject_cast<KIMAP::FetchJob *>(sender());
+    Q_ASSERT(fetch != 0);
+    Q_ASSERT(uids.size() == 1);
+    Q_ASSERT(messages.size() == 1);
+
+    Akonadi::Item i = item();
+
+    qCDebug(IMAPRESOURCE_LOG) << "MESSAGE from Imap server" << item().remoteId();
+    Q_ASSERT(item().isValid());
+
+    const qint64 number = uids.cbegin().key();
+    bool ok;
+    const Akonadi::Item remoteItem = resourceState()->messageHelper()->createItemFromMessage(messages[number], uids[number], 0, attrs.values(number), QList<QByteArray>(), fetch->scope(), ok);
+    if (!ok) {
+        qCWarning(IMAPRESOURCE_LOG) << "Failed to retrieve message " << uids[number];
+        cancelTask(i18n("No message retrieved, failed to read the message."));
+        return;
+    }
+    i.setMimeType(remoteItem.mimeType());
+    i.setPayload(remoteItem.payload<KMime::Message::Ptr>());
+    foreach (const QByteArray &flag, remoteItem.flags()) {
+        i.setFlag(flag);
+    }
+
+    qCDebug(IMAPRESOURCE_LOG) << "Has Payload: " << i.hasPayload();
+
+    m_messageReceived = true;
+    itemRetrieved(i);
+}
+
+void RetrieveItemTask::onContentFetchDone(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(job->errorString());
+    } else if (!m_messageReceived) {
+        cancelTask(i18n("No message retrieved, server reply was empty."));
+    }
+}
+
diff --git a/resources/imap/retrieveitemtask.h b/resources/imap/retrieveitemtask.h
new file mode 100644 (file)
index 0000000..47622e5
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef RETRIEVEITEMTASK_H
+#define RETRIEVEITEMTASK_H
+
+#include <kimap/fetchjob.h>
+
+#include "resourcetask.h"
+
+class RetrieveItemTask : public ResourceTask
+{
+    Q_OBJECT
+
+public:
+    explicit RetrieveItemTask(ResourceStateInterface::Ptr resource, QObject *parent = Q_NULLPTR);
+    virtual ~RetrieveItemTask();
+
+private Q_SLOTS:
+    void onSelectDone(KJob *job);
+    void onMessagesReceived(const QString &mailBox,
+                            const QMap<qint64, qint64> &uids,
+                            const QMap<qint64, KIMAP::MessageAttribute> &attrs,
+                            const QMap<qint64, KIMAP::MessagePtr> &messages);
+    void onContentFetchDone(KJob *job);
+
+protected:
+    void doStart(KIMAP::Session *session) Q_DECL_OVERRIDE;
+
+private:
+    void triggerFetchJob();
+
+    KIMAP::Session *m_session;
+    qint64 m_uid;
+    bool m_messageReceived;
+};
+
+#endif
diff --git a/resources/imap/searchtask.cpp b/resources/imap/searchtask.cpp
new file mode 100644 (file)
index 0000000..d3a0b4c
--- /dev/null
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2013  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License or (at your option) version 3 or any later version
+ * accepted by the membership of KDE e.V. (or its successor approved
+ * by the membership of KDE e.V.), which shall act as a proxy
+ * defined in Section 14 of version 3 of the license.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "searchtask.h"
+#include <kimap/searchjob.h>
+#include <kimap/session.h>
+#include <kimap/selectjob.h>
+#include <SearchQuery>
+#include <Akonadi/KMime/MessageFlags>
+#include "imapresource_debug.h"
+
+Q_DECLARE_METATYPE(KIMAP::Session *)
+
+SearchTask::SearchTask(const ResourceStateInterface::Ptr &state,  const QString &query, QObject *parent)
+    : ResourceTask(ResourceTask::DeferIfNoSession, state, parent)
+    , m_query(query)
+{
+}
+
+SearchTask::~SearchTask()
+{
+}
+
+void SearchTask::doStart(KIMAP::Session *session)
+{
+    qCDebug(IMAPRESOURCE_LOG) << collection().remoteId();
+
+    const QString mailbox = mailBoxForCollection(collection());
+    if (session->selectedMailBox() == mailbox) {
+        doSearch(session);
+        return;
+    }
+
+    KIMAP::SelectJob *select = new KIMAP::SelectJob(session);
+    select->setMailBox(mailbox);
+    connect(select, &KJob::finished,
+            this, &SearchTask::onSelectDone);
+    select->start();
+}
+
+void SearchTask::onSelectDone(KJob *job)
+{
+    if (job->error()) {
+        searchFinished(QVector<qint64>());
+        cancelTask(job->errorText());
+        return;
+    }
+
+    doSearch(qobject_cast<KIMAP::SelectJob *>(job)->session());
+}
+
+static KIMAP::Term::Relation mapRelation(Akonadi::SearchTerm::Relation relation)
+{
+    if (relation == Akonadi::SearchTerm::RelAnd) {
+        return KIMAP::Term::And;
+    }
+    return KIMAP::Term::Or;
+}
+
+static KIMAP::Term recursiveEmailTermMapping(const Akonadi::SearchTerm &term)
+{
+    if (!term.subTerms().isEmpty()) {
+        QVector<KIMAP::Term> subterms;
+        Q_FOREACH (const Akonadi::SearchTerm &subterm, term.subTerms()) {
+            const KIMAP::Term newTerm = recursiveEmailTermMapping(subterm);
+            if (!newTerm.isNull()) {
+                subterms << newTerm;
+            }
+        }
+        return KIMAP::Term(mapRelation(term.relation()), subterms);
+    } else {
+        const Akonadi::EmailSearchTerm::EmailSearchField field = Akonadi::EmailSearchTerm::fromKey(term.key());
+        switch (field) {
+        case Akonadi::EmailSearchTerm::Message:
+            return KIMAP::Term(KIMAP::Term::Text, term.value().toString()).setNegated(term.isNegated());
+        case Akonadi::EmailSearchTerm::Body:
+            return KIMAP::Term(KIMAP::Term::Body, term.value().toString()).setNegated(term.isNegated());
+        case Akonadi::EmailSearchTerm::Headers:
+            //FIXME
+//                 return KIMAP::Term(KIMAP::Term::Header, term.value()).setNegated(term.isNegated());
+            break;
+        case Akonadi::EmailSearchTerm::ByteSize: {
+            int value = term.value().toInt();
+            switch (term.condition()) {
+            case Akonadi::SearchTerm::CondGreaterOrEqual:
+                value--;
+            case Akonadi::SearchTerm::CondGreaterThan:
+                return KIMAP::Term(KIMAP::Term::Larger, value).setNegated(term.isNegated());
+            case Akonadi::SearchTerm::CondLessOrEqual:
+                value++;
+            case Akonadi::SearchTerm::CondLessThan:
+                return KIMAP::Term(KIMAP::Term::Smaller, value).setNegated(term.isNegated());
+            case Akonadi::SearchTerm::CondEqual:
+                return KIMAP::Term(KIMAP::Term::And, QVector<KIMAP::Term>() << KIMAP::Term(KIMAP::Term::Smaller, value + 1) << KIMAP::Term(KIMAP::Term::Larger, value + 1)).setNegated(term.isNegated());
+            case Akonadi::SearchTerm::CondContains:
+                qCDebug(IMAPRESOURCE_LOG) << " invalid condition for ByteSize";
+                break;
+            }
+        }
+        break;
+        case Akonadi::EmailSearchTerm::HeaderOnlyDate:
+        case Akonadi::EmailSearchTerm::HeaderDate: {
+            QDate value = term.value().toDateTime().date();
+            switch (term.condition()) {
+            case Akonadi::SearchTerm::CondGreaterOrEqual:
+                value = value.addDays(-1);
+            case Akonadi::SearchTerm::CondGreaterThan:
+                return KIMAP::Term(KIMAP::Term::SentSince, value).setNegated(term.isNegated());
+            case Akonadi::SearchTerm::CondLessOrEqual:
+                value = value.addDays(1);
+            case Akonadi::SearchTerm::CondLessThan:
+                return KIMAP::Term(KIMAP::Term::SentBefore, value).setNegated(term.isNegated());
+            case Akonadi::SearchTerm::CondEqual:
+                return KIMAP::Term(KIMAP::Term::SentOn, value).setNegated(term.isNegated());
+            case Akonadi::SearchTerm::CondContains:
+                qCDebug(IMAPRESOURCE_LOG) << " invalid condition for Date";
+                break;
+            }
+        }
+        case Akonadi::EmailSearchTerm::Subject:
+            return KIMAP::Term(KIMAP::Term::Subject, term.value().toString()).setNegated(term.isNegated());
+        case Akonadi::EmailSearchTerm::HeaderFrom:
+            return KIMAP::Term(KIMAP::Term::From, term.value().toString()).setNegated(term.isNegated());
+        case Akonadi::EmailSearchTerm::HeaderTo:
+            return KIMAP::Term(KIMAP::Term::To, term.value().toString()).setNegated(term.isNegated());
+        case Akonadi::EmailSearchTerm::HeaderCC:
+            return KIMAP::Term(KIMAP::Term::Cc, term.value().toString()).setNegated(term.isNegated());
+        case Akonadi::EmailSearchTerm::HeaderBCC:
+            return KIMAP::Term(KIMAP::Term::Bcc, term.value().toString()).setNegated(term.isNegated());
+        case Akonadi::EmailSearchTerm::MessageStatus:
+            if (term.value().toString() == QString::fromLatin1(Akonadi::MessageFlags::Flagged)) {
+                return KIMAP::Term(KIMAP::Term::Flagged).setNegated(term.isNegated());
+            }
+            if (term.value().toString() == QString::fromLatin1(Akonadi::MessageFlags::Deleted)) {
+                return KIMAP::Term(KIMAP::Term::Deleted).setNegated(term.isNegated());
+            }
+            if (term.value().toString() == QString::fromLatin1(Akonadi::MessageFlags::Replied)) {
+                return KIMAP::Term(KIMAP::Term::Answered).setNegated(term.isNegated());
+            }
+            if (term.value().toString() == QString::fromLatin1(Akonadi::MessageFlags::Seen)) {
+                return KIMAP::Term(KIMAP::Term::Seen).setNegated(term.isNegated());
+            }
+            break;
+        case Akonadi::EmailSearchTerm::MessageTag:
+            break;
+        case Akonadi::EmailSearchTerm::HeaderReplyTo:
+            break;
+        case Akonadi::EmailSearchTerm::HeaderOrganization:
+            break;
+        case Akonadi::EmailSearchTerm::HeaderListId:
+            break;
+        case Akonadi::EmailSearchTerm::HeaderResentFrom:
+            break;
+        case Akonadi::EmailSearchTerm::HeaderXLoop:
+            break;
+        case Akonadi::EmailSearchTerm::HeaderXMailingList:
+            break;
+        case Akonadi::EmailSearchTerm::HeaderXSpamFlag:
+            break;
+        case Akonadi::EmailSearchTerm::Unknown:
+        default:
+            qCWarning(IMAPRESOURCE_LOG) << "unknown term " << term.key();
+        }
+    }
+    return KIMAP::Term();
+}
+
+void SearchTask::doSearch(KIMAP::Session *session)
+{
+    qCDebug(IMAPRESOURCE_LOG) << m_query;
+
+    Akonadi::SearchQuery query = Akonadi::SearchQuery::fromJSON(m_query.toLatin1());
+    KIMAP::SearchJob *searchJob = new KIMAP::SearchJob(session);
+    searchJob->setUidBased(true);
+
+    KIMAP::Term term = recursiveEmailTermMapping(query.term());
+    if (term.isNull()) {
+        qCWarning(IMAPRESOURCE_LOG) << "failed to translate query " << m_query;
+        searchFinished(QVector<qint64>());
+        cancelTask(QStringLiteral("Invalid search"));
+        return;
+    }
+    searchJob->setTerm(term);
+
+    connect(searchJob, &KJob::finished,
+            this, &SearchTask::onSearchDone);
+    searchJob->start();
+}
+
+void SearchTask::onSearchDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Failed to execute search " << job->errorString();
+        qCDebug(IMAPRESOURCE_LOG) << m_query;
+        searchFinished(QVector<qint64>());
+        cancelTask(job->errorString());
+        return;
+    }
+
+    KIMAP::SearchJob *searchJob = qobject_cast<KIMAP::SearchJob *>(job);
+    const QVector<qint64> result = searchJob->results();
+    qCDebug(IMAPRESOURCE_LOG) << result.count() << "matches";
+
+    searchFinished(result);
+    taskDone();
+}
diff --git a/resources/imap/searchtask.h b/resources/imap/searchtask.h
new file mode 100644 (file)
index 0000000..8501fb4
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2013  Daniel Vrátil <dvratil@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License or (at your option) version 3 or any later version
+ * accepted by the membership of KDE e.V. (or its successor approved
+ * by the membership of KDE e.V.), which shall act as a proxy
+ * defined in Section 14 of version 3 of the license.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef SEARCHTASK_H
+#define SEARCHTASK_H
+
+#include "resourcetask.h"
+
+class SearchTask : public ResourceTask
+{
+    Q_OBJECT
+public:
+    SearchTask(const ResourceStateInterface::Ptr &state, const QString &query, QObject *parent);
+    ~SearchTask();
+
+protected:
+    void doStart(KIMAP::Session *session) Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void onSelectDone(KJob *job);
+    void onSearchDone(KJob *job);
+
+private:
+    void doSearch(KIMAP::Session *session);
+
+    QString m_query;
+
+};
+
+#endif // SEARCHTASK_H
diff --git a/resources/imap/serverinfo.ui b/resources/imap/serverinfo.ui
new file mode 100644 (file)
index 0000000..66a0c70
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ServerInfo</class>
+ <widget class="QWidget" name="ServerInfo">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>283</width>
+    <height>322</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Server Info</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QTextBrowser" name="serverInfo"/>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/resources/imap/serverinfodialog.cpp b/resources/imap/serverinfodialog.cpp
new file mode 100644 (file)
index 0000000..3584710
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+  Copyright (c) 2013-2016 Montel Laurent <montel@kde.org>
+
+  This library is free software; you can redistribute it and/or modify it
+  under the terms of the GNU Library General Public License as published by
+  the Free Software Foundation; either version 2 of the License, or (at your
+  option) any later version.
+
+  This library is distributed in the hope that it will be useful, but WITHOUT
+  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+  License for more details.
+
+  You should have received a copy of the GNU Library General Public License
+  along with this library; see the file COPYING.LIB.  If not, write to the
+  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+  02110-1301, USA.
+
+*/
+
+#include "serverinfodialog.h"
+#include "imapresource.h"
+#include "ui_serverinfo.h"
+
+#include <KLocalizedString>
+#include <QDialogButtonBox>
+#include <KConfigGroup>
+#include <QPushButton>
+#include <QVBoxLayout>
+
+ServerInfoDialog::ServerInfoDialog(ImapResourceBase *parentResource, QWidget *parent)
+    : QDialog(parent)
+{
+    setWindowTitle(
+        i18nc("@title:window Dialog title for dialog showing information about a server",
+              "Server Info"));
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close);
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    setLayout(mainLayout);
+    QWidget *w = new QWidget;
+    mainLayout->addWidget(w);
+    connect(buttonBox, &QDialogButtonBox::accepted, this, &ServerInfoDialog::accept);
+    connect(buttonBox, &QDialogButtonBox::rejected, this, &ServerInfoDialog::reject);
+    setAttribute(Qt::WA_DeleteOnClose);
+
+    mServerInfoWidget = new Ui::ServerInfo();
+    mServerInfoWidget->setupUi(w);
+    mainLayout->addWidget(buttonBox);
+    mServerInfoWidget->serverInfo->setPlainText(
+        parentResource->serverCapabilities().join(QStringLiteral("\n")));
+}
+
+ServerInfoDialog::~ServerInfoDialog()
+{
+    delete mServerInfoWidget;
+}
+
diff --git a/resources/imap/serverinfodialog.h b/resources/imap/serverinfodialog.h
new file mode 100644 (file)
index 0000000..0c3ca25
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+  Copyright (c) 2013-2016 Montel Laurent <montel@kde.org>
+
+  This library is free software; you can redistribute it and/or modify it
+  under the terms of the GNU Library General Public License as published by
+  the Free Software Foundation; either version 2 of the License, or (at your
+  option) any later version.
+
+  This library is distributed in the hope that it will be useful, but WITHOUT
+  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+  License for more details.
+
+  You should have received a copy of the GNU Library General Public License
+  along with this library; see the file COPYING.LIB.  If not, write to the
+  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+  02110-1301, USA.
+
+*/
+
+#ifndef SERVERINFODIALOG_H
+#define SERVERINFODIALOG_H
+
+#include <QDialog>
+
+class ImapResourceBase;
+namespace Ui
+{
+class ServerInfo;
+}
+
+class ServerInfoDialog : public QDialog
+{
+    Q_OBJECT
+public:
+    explicit ServerInfoDialog(ImapResourceBase *parentResource, QWidget *parent);
+    ~ServerInfoDialog();
+private:
+    Ui::ServerInfo *mServerInfoWidget;
+};
+
+#endif // SERVERINFODIALOG_H
diff --git a/resources/imap/sessionpool.cpp b/resources/imap/sessionpool.cpp
new file mode 100644 (file)
index 0000000..219a97f
--- /dev/null
@@ -0,0 +1,555 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "sessionpool.h"
+
+#include <QtCore/QTimer>
+#include <QtNetwork/QSslSocket>
+
+#include "imapresource_debug.h"
+#include <KLocalizedString>
+
+#include <kimap/capabilitiesjob.h>
+#include <kimap/logoutjob.h>
+#include <kimap/namespacejob.h>
+
+#include "imapaccount.h"
+#include "passwordrequesterinterface.h"
+
+qint64 SessionPool::m_requestCounter = 0;
+
+SessionPool::SessionPool(int maxPoolSize, QObject *parent)
+    : QObject(parent),
+      m_maxPoolSize(maxPoolSize),
+      m_account(Q_NULLPTR),
+      m_passwordRequester(Q_NULLPTR),
+      m_initialConnectDone(false),
+      m_pendingInitialSession(Q_NULLPTR)
+{
+}
+
+SessionPool::~SessionPool()
+{
+    disconnect(CloseSession);
+}
+
+PasswordRequesterInterface *SessionPool::passwordRequester() const
+{
+    return m_passwordRequester;
+}
+
+void SessionPool::setPasswordRequester(PasswordRequesterInterface *requester)
+{
+    delete m_passwordRequester;
+
+    m_passwordRequester = requester;
+    m_passwordRequester->setParent(this);
+    QObject::connect(m_passwordRequester, &PasswordRequesterInterface::done,
+                     this, &SessionPool::onPasswordRequestDone);
+}
+
+void SessionPool::cancelPasswordRequests()
+{
+    m_passwordRequester->cancelPasswordRequests();
+}
+
+KIMAP::SessionUiProxy::Ptr SessionPool::sessionUiProxy() const
+{
+    return m_sessionUiProxy;
+}
+
+void SessionPool::setSessionUiProxy(KIMAP::SessionUiProxy::Ptr proxy)
+{
+    m_sessionUiProxy = proxy;
+}
+
+bool SessionPool::isConnected() const
+{
+    return m_initialConnectDone;
+}
+
+bool SessionPool::connect(ImapAccount *account)
+{
+    if (m_account) {
+        return false;
+    }
+
+    m_account = account;
+    if (m_account->authenticationMode() == KIMAP::LoginJob::GSSAPI) {
+        // for GSSAPI we don't have to ask for username/password, because it uses session wide tickets
+        QMetaObject::invokeMethod(this, "onPasswordRequestDone", Qt::QueuedConnection,
+                                  Q_ARG(int, PasswordRequesterInterface::PasswordRetrieved),
+                                  Q_ARG(QString, QString()));
+    } else {
+        m_passwordRequester->requestPassword();
+    }
+
+    return true;
+}
+
+void SessionPool::disconnect(SessionTermination termination)
+{
+    if (!m_account) {
+        return;
+    }
+
+    foreach (KIMAP::Session *s, m_unusedPool + m_reservedPool + m_connectingPool) {
+        killSession(s, termination);
+    }
+    m_unusedPool.clear();
+    m_reservedPool.clear();
+    m_connectingPool.clear();
+    m_pendingInitialSession = Q_NULLPTR;
+    m_passwordRequester->cancelPasswordRequests();
+
+    delete m_account;
+    m_account = Q_NULLPTR;
+    m_namespaces.clear();
+    m_capabilities.clear();
+
+    m_initialConnectDone = false;
+    Q_EMIT disconnectDone();
+}
+
+qint64 SessionPool::requestSession()
+{
+    if (!m_initialConnectDone) {
+        return -1;
+    }
+
+    qint64 requestNumber = ++m_requestCounter;
+
+    // The queue was empty, so trigger the processing
+    if (m_pendingRequests.isEmpty()) {
+        QTimer::singleShot(0, this, &SessionPool::processPendingRequests);
+    }
+
+    m_pendingRequests << requestNumber;
+
+    return requestNumber;
+}
+
+void SessionPool::cancelSessionRequest(qint64 id)
+{
+    Q_ASSERT(id > 0);
+    m_pendingRequests.removeAll(id);
+}
+
+void SessionPool::releaseSession(KIMAP::Session *session)
+{
+    if (m_reservedPool.contains(session)) {
+        m_reservedPool.removeAll(session);
+        m_unusedPool << session;
+    }
+}
+
+ImapAccount *SessionPool::account() const
+{
+    return m_account;
+}
+
+QStringList SessionPool::serverCapabilities() const
+{
+    return m_capabilities;
+}
+
+QList<KIMAP::MailBoxDescriptor> SessionPool::serverNamespaces() const
+{
+    return m_namespaces;
+}
+
+QList<KIMAP::MailBoxDescriptor> SessionPool::serverNamespaces(Namespace ns) const
+{
+    switch (ns) {
+    case Personal:
+        return m_personalNamespaces;
+    case User:
+        return m_userNamespaces;
+    case Shared:
+        return m_sharedNamespaces;
+    default:
+        break;
+    }
+    Q_ASSERT(false);
+    return QList<KIMAP::MailBoxDescriptor>();
+}
+
+void SessionPool::killSession(KIMAP::Session *session, SessionTermination termination)
+{
+    if (!m_unusedPool.contains(session) && !m_reservedPool.contains(session) && !m_connectingPool.contains(session)) {
+        qCWarning(IMAPRESOURCE_LOG) << "Unmanaged session" << session;
+        Q_ASSERT(false);
+        return;
+    }
+    QObject::disconnect(session, &KIMAP::Session::stateChanged,
+                        this, &SessionPool::onSessionStateChanged);
+    m_unusedPool.removeAll(session);
+    m_reservedPool.removeAll(session);
+    m_connectingPool.removeAll(session);
+
+    if (session->state() != KIMAP::Session::Disconnected && termination == LogoutSession) {
+        KIMAP::LogoutJob *logout = new KIMAP::LogoutJob(session);
+        QObject::connect(logout, &KJob::result,
+                         session, &QObject::deleteLater);
+        logout->start();
+    } else {
+        session->close();
+        session->deleteLater();
+    }
+}
+
+void SessionPool::declareSessionReady(KIMAP::Session *session)
+{
+    //This can happen if we happen to disconnect while capabilities and namespace are being retrieved,
+    //resulting in us keeping a dangling pointer to a deleted session
+    if (!m_connectingPool.contains(session)) {
+        qCWarning(IMAPRESOURCE_LOG) << "Tried to declare a removed session ready";
+        return;
+    }
+
+    m_pendingInitialSession = Q_NULLPTR;
+
+    if (!m_initialConnectDone) {
+        m_initialConnectDone = true;
+        Q_EMIT connectDone();
+    }
+
+    m_connectingPool.removeAll(session);
+
+    if (m_pendingRequests.isEmpty()) {
+        m_unusedPool << session;
+    } else {
+        m_reservedPool << session;
+        Q_EMIT sessionRequestDone(m_pendingRequests.takeFirst(), session);
+
+        if (!m_pendingRequests.isEmpty()) {
+            QTimer::singleShot(0, this, &SessionPool::processPendingRequests);
+        }
+    }
+}
+
+void SessionPool::cancelSessionCreation(KIMAP::Session *session, int errorCode,
+                                        const QString &errorMessage)
+{
+    m_pendingInitialSession = Q_NULLPTR;
+
+    QString msg;
+    if (m_account) {
+        msg = i18n("Could not connect to the IMAP-server %1.\n%2", m_account->server(), errorMessage);
+    } else {
+        // Can happen when we lose all ready connections while trying to establish
+        // a new connection, for example.
+        msg = i18n("Could not connect to the IMAP server.\n%1", errorMessage);
+    }
+
+    if (!m_initialConnectDone) {
+        disconnect(); // kills all sessions, including \a session
+    } else {
+        killSession(session, LogoutSession);
+        if (!m_pendingRequests.isEmpty()) {
+            Q_EMIT sessionRequestDone(m_pendingRequests.takeFirst(), Q_NULLPTR, errorCode, errorMessage);
+            if (!m_pendingRequests.isEmpty()) {
+                QTimer::singleShot(0, this, &SessionPool::processPendingRequests);
+            }
+        }
+    }
+    //AlwaysQ_EMIT this at the end. This can call SessionPool::disconnect via ImapResource.
+    Q_EMIT connectDone(errorCode, msg);
+}
+
+void SessionPool::processPendingRequests()
+{
+    if (!m_unusedPool.isEmpty()) {
+        // We have a session ready to give out
+        KIMAP::Session *session = m_unusedPool.takeFirst();
+        m_reservedPool << session;
+        if (!m_pendingRequests.isEmpty()) {
+            Q_EMIT sessionRequestDone(m_pendingRequests.takeFirst(), session);
+            if (!m_pendingRequests.isEmpty()) {
+                QTimer::singleShot(0, this, &SessionPool::processPendingRequests);
+            }
+        }
+    } else if (m_unusedPool.size() + m_reservedPool.size() < m_maxPoolSize) {
+        // We didn't reach the max pool size yet so create a new one
+        m_passwordRequester->requestPassword();
+
+    } else {
+        // No session available, and max pool size reached
+        if (!m_pendingRequests.isEmpty()) {
+            Q_EMIT sessionRequestDone(
+                m_pendingRequests.takeFirst(), Q_NULLPTR, NoAvailableSessionError,
+                i18n("Could not create another extra connection to the IMAP-server %1.",
+                     m_account->server()));
+            if (!m_pendingRequests.isEmpty()) {
+                QTimer::singleShot(0, this, &SessionPool::processPendingRequests);
+            }
+        }
+    }
+}
+
+void SessionPool::onPasswordRequestDone(int resultType, const QString &password)
+{
+    QString errorMessage;
+
+    if (!m_account) {
+        // it looks like the connection was lost while we were waiting
+        // for the password, we should fail all the pending requests and stop there
+        foreach (int request, m_pendingRequests) {
+            Q_EMIT sessionRequestDone(request, Q_NULLPTR,
+                                      LoginFailError, i18n("Disconnected from server during login."));
+        }
+        return;
+    }
+
+    switch (resultType) {
+    case PasswordRequesterInterface::PasswordRetrieved:
+        // All is fine
+        break;
+    case PasswordRequesterInterface::ReconnectNeeded:
+        Q_ASSERT(m_pendingInitialSession != 0);
+        cancelSessionCreation(m_pendingInitialSession, ReconnectNeededError, errorMessage);
+        return;
+    case PasswordRequesterInterface::UserRejected:
+        errorMessage = i18n("Could not read the password: user rejected wallet access");
+        if (m_pendingInitialSession) {
+            cancelSessionCreation(m_pendingInitialSession, LoginFailError, errorMessage);
+        } else {
+            Q_EMIT connectDone(PasswordRequestError, errorMessage);
+        }
+        return;
+    case PasswordRequesterInterface::EmptyPasswordEntered:
+        errorMessage = i18n("Empty password");
+        if (m_pendingInitialSession) {
+            cancelSessionCreation(m_pendingInitialSession, LoginFailError, errorMessage);
+        } else {
+            Q_EMIT connectDone(PasswordRequestError, errorMessage);
+        }
+        return;
+    }
+
+    if (m_account->encryptionMode() != KIMAP::LoginJob::Unencrypted && !QSslSocket::supportsSsl()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Crypto not supported!";
+        Q_EMIT connectDone(EncryptionError,
+                           i18n("You requested TLS/SSL to connect to %1, but your "
+                                "system does not seem to be set up for that.", m_account->server()));
+        disconnect();
+        return;
+    }
+
+    KIMAP::Session *session = Q_NULLPTR;
+    if (m_pendingInitialSession) {
+        session = m_pendingInitialSession;
+    } else {
+        session = new KIMAP::Session(m_account->server(), m_account->port(), this);
+        QObject::connect(session, &QObject::destroyed, this, &SessionPool::onSessionDestroyed);
+        session->setUiProxy(m_sessionUiProxy);
+        session->setTimeout(m_account->timeout());
+        m_connectingPool << session;
+    }
+
+    QObject::connect(session, &KIMAP::Session::stateChanged,
+                     this, &SessionPool::onSessionStateChanged);
+
+    KIMAP::LoginJob *loginJob = new KIMAP::LoginJob(session);
+    loginJob->setUserName(m_account->userName());
+    loginJob->setPassword(password);
+    loginJob->setEncryptionMode(m_account->encryptionMode());
+    loginJob->setAuthenticationMode(m_account->authenticationMode());
+
+    QObject::connect(loginJob, &KJob::result,
+                     this, &SessionPool::onLoginDone);
+    loginJob->start();
+}
+
+void SessionPool::onLoginDone(KJob *job)
+{
+    KIMAP::LoginJob *login = static_cast<KIMAP::LoginJob *>(job);
+    //Can happen if we disonnected meanwhile
+    if (!m_connectingPool.contains(login->session())) {
+        Q_EMIT connectDone(CancelledError, i18n("Disconnected from server during login."));
+        return;
+    }
+
+    if (job->error() == 0) {
+        if (m_initialConnectDone) {
+            declareSessionReady(login->session());
+        } else {
+            // On initial connection we ask for capabilities
+            KIMAP::CapabilitiesJob *capJob = new KIMAP::CapabilitiesJob(login->session());
+            QObject::connect(capJob, &KIMAP::CapabilitiesJob::result, this, &SessionPool::onCapabilitiesTestDone);
+            capJob->start();
+        }
+    } else {
+        if (job->error() == KIMAP::LoginJob::ERR_COULD_NOT_CONNECT) {
+            if (m_account) {
+                cancelSessionCreation(login->session(),
+                                      CouldNotConnectError,
+                                      i18n("Could not connect to the IMAP-server %1.\n%2",
+                                           m_account->server(), job->errorString()));
+            } else {
+                // Can happen when we loose all ready connections while trying to login.
+                cancelSessionCreation(login->session(),
+                                      CouldNotConnectError,
+                                      i18n("Could not connect to the IMAP-server.\n%1",
+                                           job->errorString()));
+            }
+        } else {
+            // Connection worked, but login failed -> ask for a different password or ssl settings.
+            m_pendingInitialSession = login->session();
+            m_passwordRequester->requestPassword(PasswordRequesterInterface::WrongPasswordRequest,
+                                                 job->errorString());
+        }
+    }
+}
+
+void SessionPool::onCapabilitiesTestDone(KJob *job)
+{
+    KIMAP::CapabilitiesJob *capJob = qobject_cast<KIMAP::CapabilitiesJob *>(job);
+    //Can happen if we disonnected meanwhile
+    if (!m_connectingPool.contains(capJob->session())) {
+        Q_EMIT connectDone(CancelledError, i18n("Disconnected from server during login."));
+        return;
+    }
+
+    if (job->error()) {
+        if (m_account) {
+            cancelSessionCreation(capJob->session(),
+                                  CapabilitiesTestError,
+                                  i18n("Could not test the capabilities supported by the "
+                                       "IMAP server %1.\n%2",
+                                       m_account->server(), job->errorString()));
+        } else {
+            // Can happen when we loose all ready connections while trying to check capabilities.
+            cancelSessionCreation(capJob->session(),
+                                  CapabilitiesTestError,
+                                  i18n("Could not test the capabilities supported by the "
+                                       "IMAP server.\n%1", job->errorString()));
+        }
+        return;
+    }
+
+    m_capabilities = capJob->capabilities();
+    QStringList expected;
+    expected << QStringLiteral("IMAP4REV1");
+
+    QStringList missing;
+    foreach (const QString &capability, expected) {
+        if (!m_capabilities.contains(capability)) {
+            missing << capability;
+        }
+    }
+
+    if (!missing.isEmpty()) {
+        cancelSessionCreation(capJob->session(),
+                              IncompatibleServerError,
+                              i18n("Cannot use the IMAP server %1, "
+                                   "some mandatory capabilities are missing: %2. "
+                                   "Please ask your sysadmin to upgrade the server.",
+                                   m_account->server(),
+                                   missing.join(QStringLiteral(", "))));
+        return;
+    }
+
+    // If the extension is supported, grab the namespaces from the server
+    if (m_capabilities.contains(QStringLiteral("NAMESPACE"))) {
+        KIMAP::NamespaceJob *nsJob = new KIMAP::NamespaceJob(capJob->session());
+        QObject::connect(nsJob, &KIMAP::NamespaceJob::result, this, &SessionPool::onNamespacesTestDone);
+        nsJob->start();
+        return;
+    } else {
+        declareSessionReady(capJob->session());
+    }
+}
+
+void SessionPool::onNamespacesTestDone(KJob *job)
+{
+    KIMAP::NamespaceJob *nsJob = qobject_cast<KIMAP::NamespaceJob *>(job);
+    // Can happen if we disconnect meanwhile
+    if (!m_connectingPool.contains(nsJob->session())) {
+        Q_EMIT connectDone(CancelledError, i18n("Disconnected from server during login."));
+        return;
+    }
+
+    if (nsJob->containsEmptyNamespace()) {
+        // When we got the empty namespace here, we assume that the other
+        // ones can be freely ignored and that the server will give us all
+        // the mailboxes if we list from the empty namespace itself...
+
+        m_namespaces.clear();
+
+    } else {
+        // ... otherwise we assume that we have to list explicitly each
+        // namespace
+
+        m_namespaces = nsJob->personalNamespaces() +
+                       nsJob->userNamespaces() +
+                       nsJob->sharedNamespaces();
+    }
+
+    declareSessionReady(nsJob->session());
+}
+
+void SessionPool::onSessionStateChanged(KIMAP::Session::State newState, KIMAP::Session::State oldState)
+{
+    if (newState == KIMAP::Session::Disconnected && oldState != KIMAP::Session::Disconnected) {
+        onConnectionLost();
+    }
+}
+
+void SessionPool::onConnectionLost()
+{
+    KIMAP::Session *session = static_cast<KIMAP::Session *>(sender());
+
+    m_unusedPool.removeAll(session);
+    m_reservedPool.removeAll(session);
+    m_connectingPool.removeAll(session);
+
+    if (m_unusedPool.isEmpty() && m_reservedPool.isEmpty()) {
+        m_passwordRequester->cancelPasswordRequests();
+        delete m_account;
+        m_account = Q_NULLPTR;
+        m_namespaces.clear();
+        m_capabilities.clear();
+
+        m_initialConnectDone = false;
+    }
+
+    Q_EMIT connectionLost(session);
+
+    session->deleteLater();
+    if (session == m_pendingInitialSession) {
+        m_pendingInitialSession = Q_NULLPTR;
+    }
+}
+
+void SessionPool::onSessionDestroyed(QObject *object)
+{
+    //Safety net for bugs that cause dangling session pointers
+    KIMAP::Session *session = static_cast<KIMAP::Session *>(object);
+    if (m_unusedPool.contains(session) || m_reservedPool.contains(session) || m_connectingPool.contains(session)) {
+        qCWarning(IMAPRESOURCE_LOG) << "Session destroyed while still in pool" << session;
+        m_unusedPool.removeAll(session);
+        m_reservedPool.removeAll(session);
+        m_connectingPool.removeAll(session);
+        Q_ASSERT(false);
+    }
+}
+
diff --git a/resources/imap/sessionpool.h b/resources/imap/sessionpool.h
new file mode 100644 (file)
index 0000000..7b9c1c0
--- /dev/null
@@ -0,0 +1,139 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef SESSIONPOOL_H
+#define SESSIONPOOL_H
+
+#include <QtCore/QObject>
+#include <QtCore/QStringList>
+
+#include <kimap/listjob.h>
+#include <kimap/session.h>
+#include <kimap/sessionuiproxy.h>
+
+namespace KIMAP
+{
+class MailBoxDescriptor;
+}
+
+class ImapAccount;
+class PasswordRequesterInterface;
+
+class SessionPool : public QObject
+{
+    Q_OBJECT
+    Q_ENUMS(ConnectError)
+
+public:
+    enum ErrorCodes {
+        NoError,
+        PasswordRequestError,
+        ReconnectNeededError,
+        EncryptionError,
+        LoginFailError,
+        CapabilitiesTestError,
+        IncompatibleServerError,
+        NoAvailableSessionError,
+        CouldNotConnectError,
+        CancelledError
+    };
+
+    enum SessionTermination {
+        LogoutSession,
+        CloseSession
+    };
+
+    explicit SessionPool(int maxPoolSize, QObject *parent = Q_NULLPTR);
+    ~SessionPool();
+
+    PasswordRequesterInterface *passwordRequester() const;
+    void setPasswordRequester(PasswordRequesterInterface *requester);
+    void cancelPasswordRequests();
+
+    KIMAP::SessionUiProxy::Ptr sessionUiProxy() const;
+    void setSessionUiProxy(KIMAP::SessionUiProxy::Ptr proxy);
+
+    bool isConnected() const;
+    bool connect(ImapAccount *account);
+    void disconnect(SessionTermination termination = LogoutSession);
+
+    qint64 requestSession();
+    void cancelSessionRequest(qint64 id);
+    void releaseSession(KIMAP::Session *session);
+
+    ImapAccount *account() const;
+    QStringList serverCapabilities() const;
+    QList<KIMAP::MailBoxDescriptor> serverNamespaces() const;
+    enum Namespace {
+        Personal,
+        User,
+        Shared
+    };
+    QList<KIMAP::MailBoxDescriptor> serverNamespaces(Namespace) const;
+
+Q_SIGNALS:
+    void connectionLost(KIMAP::Session *session);
+
+    void sessionRequestDone(qint64 requestNumber, KIMAP::Session *session,
+                            int errorCode = NoError, const QString &errorString = QString());
+    void connectDone(int errorCode = NoError, const QString &errorString = QString());
+    void disconnectDone();
+
+private Q_SLOTS:
+    void processPendingRequests();
+
+    void onPasswordRequestDone(int resultType, const QString &password);
+    void onLoginDone(KJob *job);
+    void onCapabilitiesTestDone(KJob *job);
+    void onNamespacesTestDone(KJob *job);
+
+    void onSessionStateChanged(KIMAP::Session::State newState, KIMAP::Session::State oldState);
+    void onSessionDestroyed(QObject *);
+
+private:
+    void onConnectionLost();
+    void killSession(KIMAP::Session *session, SessionTermination termination);
+    void declareSessionReady(KIMAP::Session *session);
+    void cancelSessionCreation(KIMAP::Session *session, int errorCode, const QString &errorString);
+
+    static qint64 m_requestCounter;
+
+    int m_maxPoolSize;
+    ImapAccount *m_account;
+    PasswordRequesterInterface *m_passwordRequester;
+    KIMAP::SessionUiProxy::Ptr m_sessionUiProxy;
+
+    bool m_initialConnectDone;
+    KIMAP::Session *m_pendingInitialSession;
+
+    QList<qint64> m_pendingRequests;
+    QList<KIMAP::Session *> m_connectingPool; // in preparation
+    QList<KIMAP::Session *> m_unusedPool;    // ready to be used
+    QList<KIMAP::Session *> m_reservedPool;  // currently used
+
+    QStringList m_capabilities;
+    QList<KIMAP::MailBoxDescriptor> m_namespaces;
+    QList<KIMAP::MailBoxDescriptor> m_personalNamespaces;
+    QList<KIMAP::MailBoxDescriptor> m_userNamespaces;
+    QList<KIMAP::MailBoxDescriptor> m_sharedNamespaces;
+};
+
+#endif
diff --git a/resources/imap/sessionuiproxy.h b/resources/imap/sessionuiproxy.h
new file mode 100644 (file)
index 0000000..24e7a4b
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef SESSIONUIPROXY_H
+#define SESSIONUIPROXY_H
+
+#include <kio/sslui.h>
+#include <kimap/sessionuiproxy.h>
+
+class SessionUiProxy : public KIMAP::SessionUiProxy
+{
+public:
+    bool ignoreSslError(const KSslErrorUiData &errorData) Q_DECL_OVERRIDE {
+        if (KIO::SslUi::askIgnoreSslErrors(errorData, KIO::SslUi::RecallAndStoreRules))
+        {
+            return true;
+        } else {
+            return false;
+        }
+    }
+};
+
+#endif
diff --git a/resources/imap/settings.cpp b/resources/imap/settings.cpp
new file mode 100644 (file)
index 0000000..127e3c2
--- /dev/null
@@ -0,0 +1,338 @@
+/*
+    Copyright (c) 2008 Volker Krause <vkrause@kde.org>
+    Copyright (c) 2008 Omat Holding B.V. <info@omat.nl>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "settings.h"
+#include "settingsadaptor.h"
+
+#include "imapaccount.h"
+
+#include <kwallet.h>
+using KWallet::Wallet;
+
+#include <KLocalizedString>
+#include <kpassworddialog.h>
+#include "imapresource_debug.h"
+
+#include <QDBusConnection>
+
+#include <Collection>
+#include <CollectionFetchJob>
+#include <CollectionModifyJob>
+
+/**
+ * Maps the enum used to represent authentication in MailTransport (kdepimlibs)
+ * to the one used by the imap resource.
+ * @param authType the MailTransport auth enum value
+ * @return the corresponding KIMAP auth value.
+ * @note will cause fatal error if there is no mapping, so be careful not to pass invalid auth options (e.g., APOP) to this function.
+ */
+KIMAP::LoginJob::AuthenticationMode Settings::mapTransportAuthToKimap(MailTransport::Transport::EnumAuthenticationType::type authType)
+{
+    // typedef these for readability
+    typedef MailTransport::Transport::EnumAuthenticationType MTAuth;
+    typedef KIMAP::LoginJob KIAuth;
+    switch (authType) {
+    case MTAuth::ANONYMOUS:
+        return KIAuth::Anonymous;
+    case MTAuth::PLAIN:
+        return KIAuth::Plain;
+    case MTAuth::NTLM:
+        return KIAuth::NTLM;
+    case MTAuth::LOGIN:
+        return KIAuth::Login;
+    case MTAuth::GSSAPI:
+        return KIAuth::GSSAPI;
+    case MTAuth::DIGEST_MD5:
+        return KIAuth::DigestMD5;
+    case MTAuth::CRAM_MD5:
+        return KIAuth::CramMD5;
+    case MTAuth::CLEAR:
+        return KIAuth::ClearText;
+    default:
+        qFatal("mapping from Transport::EnumAuthenticationType ->  KIMAP::LoginJob::AuthenticationMode not possible");
+    }
+    return KIAuth::ClearText; // dummy value, shouldn't get here.
+}
+
+Settings::Settings(WId winId) : SettingsBase(), m_winId(winId)
+{
+    load();
+
+    new SettingsAdaptor(this);
+    QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"), this, QDBusConnection::ExportAdaptors | QDBusConnection::ExportScriptableContents);
+}
+
+void Settings::setWinId(WId winId)
+{
+    m_winId = winId;
+}
+
+void Settings::clearCachedPassword()
+{
+    m_password.clear();
+}
+
+void Settings::cleanup()
+{
+    Wallet *wallet = Wallet::openWallet(Wallet::NetworkWallet(), m_winId);
+    if (wallet && wallet->isOpen()) {
+        if (wallet->hasFolder(QStringLiteral("imap"))) {
+            wallet->setFolder(QStringLiteral("imap"));
+            wallet->removeEntry(config()->name());
+        }
+        delete wallet;
+    }
+}
+
+void Settings::requestPassword()
+{
+    if (!m_password.isEmpty() ||
+            (mapTransportAuthToKimap((MailTransport::TransportBase::EnumAuthenticationType::type)authentication()) == KIMAP::LoginJob::GSSAPI)) {
+        Q_EMIT passwordRequestCompleted(m_password, false);
+    } else {
+        Wallet *wallet = Wallet::openWallet(Wallet::NetworkWallet(), m_winId, Wallet::Asynchronous);
+        if (wallet) {
+            connect(wallet, &KWallet::Wallet::walletOpened,
+                    this, &Settings::onWalletOpened);
+        } else {
+            QMetaObject::invokeMethod(this, "onWalletOpened", Qt::QueuedConnection, Q_ARG(bool, true));
+        }
+    }
+}
+
+void Settings::onWalletOpened(bool success)
+{
+    if (!success) {
+        Q_EMIT passwordRequestCompleted(QString(), true);
+    } else {
+        Wallet *wallet = qobject_cast<Wallet *>(sender());
+        bool passwordNotStoredInWallet = true;
+        if (wallet && wallet->hasFolder(QStringLiteral("imap"))) {
+            wallet->setFolder(QStringLiteral("imap"));
+            wallet->readPassword(config()->name(), m_password);
+            passwordNotStoredInWallet = false;
+        }
+        if (passwordNotStoredInWallet || m_password.isEmpty()) {
+            requestManualAuth();
+        } else {
+            Q_EMIT passwordRequestCompleted(m_password, passwordNotStoredInWallet);
+        }
+
+        if (wallet) {
+            wallet->deleteLater();
+        }
+    }
+}
+
+void Settings::requestManualAuth()
+{
+    KPasswordDialog *dlg = new KPasswordDialog(Q_NULLPTR);
+    dlg->setModal(true);
+    dlg->setPrompt(i18n("Please enter password for user '%1' on IMAP server '%2'.",
+                        userName(), imapServer()));
+    dlg->setAttribute(Qt::WA_DeleteOnClose);
+    connect(dlg, &KPasswordDialog::finished, this, &Settings::onDialogFinished);
+    dlg->show();
+}
+
+void Settings::onDialogFinished(int result)
+{
+    if (result == QDialog::Accepted) {
+        KPasswordDialog *dlg = qobject_cast<KPasswordDialog *>(sender());
+        setPassword(dlg->password());
+        Q_EMIT passwordRequestCompleted(dlg->password(), false);
+    } else {
+        Q_EMIT passwordRequestCompleted(QString(), true);
+    }
+}
+
+QString Settings::password(bool *userRejected) const
+{
+    if (userRejected != Q_NULLPTR) {
+        *userRejected = false;
+    }
+
+    if (!m_password.isEmpty() ||
+            (mapTransportAuthToKimap((MailTransport::TransportBase::EnumAuthenticationType::type)authentication()) == KIMAP::LoginJob::GSSAPI)) {
+        return m_password;
+    }
+    Wallet *wallet = Wallet::openWallet(Wallet::NetworkWallet(), m_winId);
+    if (wallet && wallet->isOpen()) {
+        if (wallet->hasFolder(QStringLiteral("imap"))) {
+            wallet->setFolder(QStringLiteral("imap"));
+            wallet->readPassword(config()->name(), m_password);
+        } else {
+            wallet->createFolder(QStringLiteral("imap"));
+        }
+    } else if (userRejected != Q_NULLPTR) {
+        *userRejected = true;
+    }
+    delete wallet;
+    return m_password;
+}
+
+QString Settings::sieveCustomPassword(bool *userRejected) const
+{
+    if (userRejected != Q_NULLPTR) {
+        *userRejected = false;
+    }
+
+    if (!m_customSievePassword.isEmpty()) {
+        return m_customSievePassword;
+    }
+
+    Wallet *wallet = Wallet::openWallet(Wallet::NetworkWallet(), m_winId);
+    if (wallet && wallet->isOpen()) {
+        if (wallet->hasFolder(QStringLiteral("imap"))) {
+            wallet->setFolder(QStringLiteral("imap"));
+            wallet->readPassword(QStringLiteral("custom_sieve_") + config()->name(), m_customSievePassword);
+        } else {
+            wallet->createFolder(QStringLiteral("imap"));
+        }
+    } else if (userRejected != Q_NULLPTR) {
+        *userRejected = true;
+    }
+    delete wallet;
+    return m_customSievePassword;
+}
+
+void Settings::setSieveCustomPassword(const QString &password)
+{
+    if (m_customSievePassword == password) {
+        return;
+    }
+    m_customSievePassword = password;
+    Wallet *wallet = Wallet::openWallet(Wallet::NetworkWallet(), m_winId);
+    if (wallet && wallet->isOpen()) {
+        if (!wallet->hasFolder(QStringLiteral("imap"))) {
+            wallet->createFolder(QStringLiteral("imap"));
+        }
+        wallet->setFolder(QStringLiteral("imap"));
+        wallet->writePassword(QLatin1String("custom_sieve_") + config()->name(), password);
+        qCDebug(IMAPRESOURCE_LOG) << "Wallet save: " << wallet->sync();
+    }
+    delete wallet;
+}
+
+void Settings::setPassword(const QString &password)
+{
+    if (password == m_password) {
+        return;
+    }
+
+    if (mapTransportAuthToKimap((MailTransport::TransportBase::EnumAuthenticationType::type)authentication()) == KIMAP::LoginJob::GSSAPI) {
+        return;
+    }
+
+    m_password = password;
+    Wallet *wallet = Wallet::openWallet(Wallet::NetworkWallet(), m_winId);
+    if (wallet && wallet->isOpen()) {
+        if (!wallet->hasFolder(QStringLiteral("imap"))) {
+            wallet->createFolder(QStringLiteral("imap"));
+        }
+        wallet->setFolder(QStringLiteral("imap"));
+        wallet->writePassword(config()->name(), password);
+        qCDebug(IMAPRESOURCE_LOG) << "Wallet save: " << wallet->sync();
+    }
+    delete wallet;
+}
+
+void Settings::loadAccount(ImapAccount *account) const
+{
+    account->setServer(imapServer());
+    if (imapPort() >= 0) {
+        account->setPort(imapPort());
+    }
+
+    account->setUserName(userName());
+    account->setSubscriptionEnabled(subscriptionEnabled());
+
+    const QString encryption = safety();
+    if (encryption == QLatin1String("SSL")) {
+        account->setEncryptionMode(KIMAP::LoginJob::AnySslVersion);
+    } else if (encryption == QLatin1String("STARTTLS")) {
+        //KIMAP confused TLS and STARTTLS, TlsV1 really means "use STARTTLS"
+        account->setEncryptionMode(KIMAP::LoginJob::TlsV1);
+    } else {
+        account->setEncryptionMode(KIMAP::LoginJob::Unencrypted);
+    }
+
+    //Some SSL Server fail to advertise an ssl version they support (AnySslVersion),
+    //we therefore allow overriding this in the config
+    //(so we don't have to make the UI unnecessarily complex for properly working servers).
+    const QString overrideEncryptionMode = overrideEncryption();
+    if (!overrideEncryptionMode.isEmpty()) {
+        qCWarning(IMAPRESOURCE_LOG) << "Overriding encryption mode with: " << overrideEncryptionMode;
+        if (overrideEncryptionMode == QLatin1String("SSLV2")) {
+            account->setEncryptionMode(KIMAP::LoginJob::SslV2);
+        } else if (overrideEncryptionMode == QLatin1String("SSLV3")) {
+            account->setEncryptionMode(KIMAP::LoginJob::SslV3);
+        } else if (overrideEncryptionMode == QLatin1String("TLSV1")) {
+            account->setEncryptionMode(KIMAP::LoginJob::SslV3_1);
+        } else if (overrideEncryptionMode == QLatin1String("SSL")) {
+            account->setEncryptionMode(KIMAP::LoginJob::AnySslVersion);
+        } else if (overrideEncryptionMode == QLatin1String("STARTTLS")) {
+            account->setEncryptionMode(KIMAP::LoginJob::TlsV1);
+        } else if (overrideEncryptionMode == QLatin1String("UNENCRYPTED")) {
+            account->setEncryptionMode(KIMAP::LoginJob::Unencrypted);
+        } else {
+            qCWarning(IMAPRESOURCE_LOG) << "Tried to force invalid encryption mode: " << overrideEncryptionMode;
+        }
+    }
+
+    account->setAuthenticationMode(
+        mapTransportAuthToKimap(
+            (MailTransport::TransportBase::EnumAuthenticationType::type) authentication()
+        )
+    );
+
+    account->setTimeout(sessionTimeout());
+
+}
+
+QString Settings::rootRemoteId() const
+{
+    return QStringLiteral("imap://") + userName() + QLatin1Char('@') + imapServer() + QLatin1Char('/');
+}
+
+void Settings::renameRootCollection(const QString &newName)
+{
+    Akonadi::Collection rootCollection;
+    rootCollection.setRemoteId(rootRemoteId());
+    Akonadi::CollectionFetchJob *fetchJob =
+        new Akonadi::CollectionFetchJob(rootCollection, Akonadi::CollectionFetchJob::Base);
+    fetchJob->setProperty("collectionName", newName);
+    connect(fetchJob, &KJob::result,
+            this, &Settings::onRootCollectionFetched);
+}
+
+void Settings::onRootCollectionFetched(KJob *job)
+{
+    const QString newName = job->property("collectionName").toString();
+    Q_ASSERT(!newName.isEmpty());
+    Akonadi::CollectionFetchJob *fetchJob = static_cast<Akonadi::CollectionFetchJob *>(job);
+    if (fetchJob->collections().size() == 1) {
+        Akonadi::Collection rootCollection = fetchJob->collections().at(0);
+        rootCollection.setName(newName);
+        new Akonadi::CollectionModifyJob(rootCollection);
+        // We don't care about the result here, nothing we can/should do if the renaming fails
+    }
+}
+
diff --git a/resources/imap/settings.h b/resources/imap/settings.h
new file mode 100644 (file)
index 0000000..2cf7765
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+    Copyright (c) 2008 Volker Krause <vkrause@kde.org>
+    Copyright (c) 2008 Omat Holding B.V. <info@omat.nl>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef SETTINGS_H
+#define SETTINGS_H
+
+#include "settingsbase.h"
+
+#include <kimap/loginjob.h>
+
+#include <mailtransport/transport.h>
+
+class ImapAccount;
+
+class Settings : public SettingsBase
+{
+    Q_OBJECT
+    Q_CLASSINFO("D-Bus Interface", "org.kde.Akonadi.Imap.Wallet")
+public:
+    static KIMAP::LoginJob::AuthenticationMode mapTransportAuthToKimap(MailTransport::Transport::EnumAuthenticationType::type authType);
+
+    explicit Settings(WId = 0);
+    void setWinId(WId);
+
+    virtual void requestPassword();
+    virtual void requestManualAuth();
+
+    virtual void loadAccount(ImapAccount *account) const;
+
+    QString rootRemoteId() const;
+    virtual void renameRootCollection(const QString &newName);
+
+    virtual void clearCachedPassword();
+    virtual void cleanup();
+
+Q_SIGNALS:
+    void passwordRequestCompleted(const QString &password, bool userRejected);
+
+public Q_SLOTS:
+    Q_SCRIPTABLE virtual QString password(bool *userRejected = Q_NULLPTR) const;
+    Q_SCRIPTABLE virtual void setPassword(const QString &password);
+    Q_SCRIPTABLE virtual void setSieveCustomPassword(const QString &password);
+    Q_SCRIPTABLE virtual QString sieveCustomPassword(bool *userRejected = Q_NULLPTR) const;
+
+protected Q_SLOTS:
+    virtual void onWalletOpened(bool success);
+    virtual void onDialogFinished(int result);
+
+    void onRootCollectionFetched(KJob *job);
+
+protected:
+    WId m_winId;
+    mutable QString m_password;
+    mutable QString m_customSievePassword;
+};
+
+#endif
diff --git a/resources/imap/settingsbase.kcfgc b/resources/imap/settingsbase.kcfgc
new file mode 100644 (file)
index 0000000..57b9d5f
--- /dev/null
@@ -0,0 +1,6 @@
+File=imapresource.kcfg
+ClassName=SettingsBase
+Mutators=true
+SetUserTexts=true
+Singleton=false
+GlobalEnums=true
diff --git a/resources/imap/settingspasswordrequester.cpp b/resources/imap/settingspasswordrequester.cpp
new file mode 100644 (file)
index 0000000..313e402
--- /dev/null
@@ -0,0 +1,157 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                         a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "settingspasswordrequester.h"
+
+#include <KMessageBox>
+#include <KLocalizedString>
+#include <QDialog>
+
+#include <mailtransport/transportbase.h>
+#include <kwindowsystem.h>
+#include "imapresource_debug.h"
+#include <KConfigGroup>
+#include <QDialogButtonBox>
+#include <QPushButton>
+#include <QVBoxLayout>
+
+#include "imapresourcebase.h"
+#include "settings.h"
+
+SettingsPasswordRequester::SettingsPasswordRequester(ImapResourceBase *resource, QObject *parent)
+    : PasswordRequesterInterface(parent), m_resource(resource), m_requestDialog(Q_NULLPTR), m_settingsDialog(Q_NULLPTR)
+{
+
+}
+
+SettingsPasswordRequester::~SettingsPasswordRequester()
+{
+    cancelPasswordRequests();
+}
+
+void SettingsPasswordRequester::requestPassword(RequestType request, const QString &serverError)
+{
+    if (request == WrongPasswordRequest) {
+        QMetaObject::invokeMethod(this, "askUserInput", Qt::QueuedConnection, Q_ARG(QString, serverError));
+    } else {
+        connect(m_resource->settings(), &Settings::passwordRequestCompleted,
+                this, &SettingsPasswordRequester::onPasswordRequestCompleted);
+        m_resource->settings()->requestPassword();
+    }
+}
+
+void SettingsPasswordRequester::askUserInput(const QString &serverError)
+{
+    // the credentials were not ok, allow to retry or change password
+    if (m_requestDialog) {
+        qCDebug(IMAPRESOURCE_LOG) << "Password request dialog is already open";
+        return;
+    }
+    QWidget *parent = QWidget::find(m_resource->winIdForDialogs());
+    QString text = i18n("The server for account \"%2\" refused the supplied username and password. "
+                        "Do you want to go to the settings, have another attempt "
+                        "at logging in, or do nothing?\n\n"
+                        "%1", serverError, m_resource->name());
+    QDialog *dialog = new QDialog(parent, Qt::Dialog);
+    dialog->setWindowTitle(i18n("Could Not Authenticate"));
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::No | QDialogButtonBox::Yes);
+    buttonBox->button(QDialogButtonBox::Yes)->setDefault(true);
+
+    buttonBox->button(QDialogButtonBox::Yes)->setText(i18n("Account Settings"));
+    buttonBox->button(QDialogButtonBox::No)->setText(i18nc("Input username/password manually and not store them", "Try Again"));
+    dialog->setAttribute(Qt::WA_DeleteOnClose);
+    connect(buttonBox->button(QDialogButtonBox::Yes), &QPushButton::clicked, this, &SettingsPasswordRequester::slotYesClicked);
+    connect(buttonBox->button(QDialogButtonBox::No), &QPushButton::clicked, this, &SettingsPasswordRequester::slotNoClicked);
+    connect(buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &SettingsPasswordRequester::slotCancelClicked);
+
+    connect(dialog, &QDialog::destroyed, this, &SettingsPasswordRequester::onDialogDestroyed);
+    m_requestDialog = dialog;
+    KWindowSystem::setMainWindow(dialog, m_resource->winIdForDialogs());
+    bool checkboxResult = false;
+    KMessageBox::createKMessageBox(dialog, buttonBox, QMessageBox::Information,
+                                   text, QStringList(),
+                                   QString(),
+                                   &checkboxResult, KMessageBox::NoExec);
+    dialog->show();
+}
+
+void SettingsPasswordRequester::onDialogDestroyed()
+{
+    m_requestDialog = Q_NULLPTR;
+}
+
+void SettingsPasswordRequester::slotNoClicked()
+{
+    connect(m_resource->settings(), &Settings::passwordRequestCompleted,
+            this, &SettingsPasswordRequester::onPasswordRequestCompleted);
+    m_resource->settings()->requestManualAuth();
+    m_requestDialog = Q_NULLPTR;
+}
+
+void SettingsPasswordRequester::slotYesClicked()
+{
+    if (!m_settingsDialog) {
+        QDialog *dialog = m_resource->createConfigureDialog(m_resource->winIdForDialogs());
+        connect(dialog, &QDialog::finished, this, &SettingsPasswordRequester::onSettingsDialogFinished);
+        m_settingsDialog = dialog;
+        dialog->show();
+    }
+    m_requestDialog = Q_NULLPTR;
+}
+
+void SettingsPasswordRequester::slotCancelClicked()
+{
+    Q_EMIT done(UserRejected);
+    m_requestDialog = Q_NULLPTR;
+}
+
+void SettingsPasswordRequester::onSettingsDialogFinished(int result)
+{
+    m_settingsDialog = Q_NULLPTR;
+    if (result == QDialog::Accepted) {
+        Q_EMIT done(ReconnectNeeded);
+    } else {
+        Q_EMIT done(UserRejected);
+    }
+}
+
+void SettingsPasswordRequester::cancelPasswordRequests()
+{
+    if (m_requestDialog) {
+        if (m_requestDialog->close()) {
+            m_requestDialog = Q_NULLPTR;
+        }
+    }
+}
+
+void SettingsPasswordRequester::onPasswordRequestCompleted(const QString &password, bool userRejected)
+{
+    disconnect(m_resource->settings(), &Settings::passwordRequestCompleted,
+               this, &SettingsPasswordRequester::onPasswordRequestCompleted);
+
+    if (userRejected) {
+        Q_EMIT done(UserRejected);
+    } else if (password.isEmpty() && (m_resource->settings()->authentication() != MailTransport::Transport::EnumAuthenticationType::GSSAPI)) {
+        Q_EMIT done(EmptyPasswordEntered);
+    } else {
+        Q_EMIT done(PasswordRetrieved, password);
+    }
+}
diff --git a/resources/imap/settingspasswordrequester.h b/resources/imap/settingspasswordrequester.h
new file mode 100644 (file)
index 0000000..26aaee3
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef SETTINGSPASSWORDREQUESTER_H
+#define SETTINGSPASSWORDREQUESTER_H
+
+#include <passwordrequesterinterface.h>
+#include <QDialog>
+
+class ImapResourceBase;
+
+class SettingsPasswordRequester : public PasswordRequesterInterface
+{
+    Q_OBJECT
+
+public:
+    explicit SettingsPasswordRequester(ImapResourceBase *resource, QObject *parent = Q_NULLPTR);
+    virtual ~SettingsPasswordRequester();
+
+    virtual void requestPassword(RequestType request = StandardRequest,
+                                 const QString &serverError = QString()) Q_DECL_OVERRIDE;
+    void cancelPasswordRequests() Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void askUserInput(const QString &serverError);
+    void onPasswordRequestCompleted(const QString &password, bool userRejected);
+    void onDialogDestroyed();
+    void slotCancelClicked();
+    void slotYesClicked();
+    void slotNoClicked();
+    void onSettingsDialogFinished(int result);
+
+private:
+    ImapResourceBase *m_resource;
+    QDialog *m_requestDialog;
+    QDialog *m_settingsDialog;
+};
+
+#endif
diff --git a/resources/imap/setupserver.cpp b/resources/imap/setupserver.cpp
new file mode 100644 (file)
index 0000000..cf2a30b
--- /dev/null
@@ -0,0 +1,670 @@
+/* This file is part of the KDE project
+
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                      a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   Copyright (C) 2010 Casey Link <unnamedrambler@gmail.com>
+   Copyright (c) 2009-2010 Klaralvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
+   Copyright (C) 2009 Kevin Ottens <ervin@kde.org>
+   Copyright (C) 2006-2008 Omat Holding B.V. <info@omat.nl>
+   Copyright (C) 2006 Frode M. Døving <frode@lnix.net>
+
+   Original copied from showfoto:
+    Copyright 2005 by Gilles Caulier <caulier.gilles@free.fr>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "setupserver.h"
+#include "settings.h"
+#include "imapresource.h"
+#include "serverinfodialog.h"
+#include "folderarchivesettingpage.h"
+
+#include <mailtransport/transport.h>
+#include <mailtransport/servertest.h>
+
+#include <kmime/kmime_message.h>
+
+#include <collectionfetchjob.h>
+#include <Akonadi/KMime/SpecialMailCollections>
+#include <Akonadi/KMime/SpecialMailCollectionsRequestJob>
+#include <resourcesettings.h>
+#include <entitydisplayattribute.h>
+#include <CollectionModifyJob>
+#include <kemailsettings.h>
+#include <KLocalizedString>
+#include <qpushbutton.h>
+#include <kmessagebox.h>
+#include <kuser.h>
+#include "imapresource_debug.h"
+#include <QNetworkConfigurationManager>
+
+#include <kidentitymanagement/identitymanager.h>
+#include <kidentitymanagement/identitycombo.h>
+#include <QVBoxLayout>
+
+#include "imapaccount.h"
+#include "subscriptiondialog.h"
+
+#include "ui_setupserverview_desktop.h"
+#include "ui_serverinfo.h"
+
+/** static helper functions **/
+static QString authenticationModeString(MailTransport::Transport::EnumAuthenticationType::type mode)
+{
+    switch (mode) {
+    case  MailTransport::Transport::EnumAuthenticationType::LOGIN:
+        return QStringLiteral("LOGIN");
+    case MailTransport::Transport::EnumAuthenticationType::PLAIN:
+        return QStringLiteral("PLAIN");
+    case MailTransport::Transport::EnumAuthenticationType::CRAM_MD5:
+        return QStringLiteral("CRAM-MD5");
+    case MailTransport::Transport::EnumAuthenticationType::DIGEST_MD5:
+        return QStringLiteral("DIGEST-MD5");
+    case MailTransport::Transport::EnumAuthenticationType::GSSAPI:
+        return QStringLiteral("GSSAPI");
+    case MailTransport::Transport::EnumAuthenticationType::NTLM:
+        return QStringLiteral("NTLM");
+    case MailTransport::Transport::EnumAuthenticationType::CLEAR:
+        return i18nc("Authentication method", "Clear text");
+    case MailTransport::Transport::EnumAuthenticationType::ANONYMOUS:
+        return i18nc("Authentication method", "Anonymous");
+    default:
+        break;
+    }
+    return QString();
+}
+
+static void setCurrentAuthMode(QComboBox *authCombo, MailTransport::Transport::EnumAuthenticationType::type authtype)
+{
+    qCDebug(IMAPRESOURCE_LOG) << "setting authcombo: " << authenticationModeString(authtype);
+    int index = authCombo->findData(authtype);
+    if (index == -1) {
+        qCWarning(IMAPRESOURCE_LOG) << "desired authmode not in the combo";
+    }
+    qCDebug(IMAPRESOURCE_LOG) << "found corresponding index: " << index << "with data" << authenticationModeString((MailTransport::Transport::EnumAuthenticationType::type) authCombo->itemData(index).toInt());
+    authCombo->setCurrentIndex(index);
+    MailTransport::Transport::EnumAuthenticationType::type t = (MailTransport::Transport::EnumAuthenticationType::type) authCombo->itemData(authCombo->currentIndex()).toInt();
+    qCDebug(IMAPRESOURCE_LOG) << "selected auth mode:" << authenticationModeString(t);
+    Q_ASSERT(t == authtype);
+}
+
+static MailTransport::Transport::EnumAuthenticationType::type getCurrentAuthMode(QComboBox *authCombo)
+{
+    MailTransport::Transport::EnumAuthenticationType::type authtype = (MailTransport::Transport::EnumAuthenticationType::type) authCombo->itemData(authCombo->currentIndex()).toInt();
+    qCDebug(IMAPRESOURCE_LOG) << "current auth mode: " << authenticationModeString(authtype);
+    return authtype;
+}
+
+static void addAuthenticationItem(QComboBox *authCombo, MailTransport::Transport::EnumAuthenticationType::type authtype)
+{
+    qCDebug(IMAPRESOURCE_LOG) << "adding auth item " << authenticationModeString(authtype);
+    authCombo->addItem(authenticationModeString(authtype), QVariant(authtype));
+}
+
+SetupServer::SetupServer(ImapResourceBase *parentResource, WId parent)
+    : QDialog(), m_parentResource(parentResource), m_ui(new Ui::SetupServerView), m_serverTest(Q_NULLPTR),
+      m_subscriptionsChanged(false), m_shouldClearCache(false), mValidator(this)
+{
+    QNetworkConfigurationManager *networkConfigMgr = new QNetworkConfigurationManager(QCoreApplication::instance());
+
+    m_parentResource->settings()->setWinId(parent);
+    QWidget *mainWidget = new QWidget(this);
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    setLayout(mainLayout);
+    mainLayout->addWidget(mainWidget);
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    mOkButton = buttonBox->button(QDialogButtonBox::Ok);
+    mOkButton->setDefault(true);
+    mOkButton->setShortcut(Qt::CTRL | Qt::Key_Return);
+    connect(buttonBox, &QDialogButtonBox::accepted, this, &SetupServer::accept);
+    connect(buttonBox, &QDialogButtonBox::rejected, this, &SetupServer::reject);
+    mainLayout->addWidget(buttonBox);
+
+    m_ui->setupUi(mainWidget);
+    m_folderArchiveSettingPage = new FolderArchiveSettingPage(m_parentResource->identifier());
+    m_ui->tabWidget->addTab(m_folderArchiveSettingPage, i18n("Archive Folder"));
+    m_ui->safeImapGroup->setId(m_ui->noRadio, KIMAP::LoginJob::Unencrypted);
+    m_ui->safeImapGroup->setId(m_ui->sslRadio, KIMAP::LoginJob::AnySslVersion);
+    m_ui->safeImapGroup->setId(m_ui->tlsRadio, KIMAP::LoginJob::TlsV1);
+
+    connect(m_ui->noRadio, &QRadioButton::toggled, this, &SetupServer::slotSafetyChanged);
+    connect(m_ui->sslRadio, &QRadioButton::toggled, this, &SetupServer::slotSafetyChanged);
+    connect(m_ui->tlsRadio, &QRadioButton::toggled, this, &SetupServer::slotSafetyChanged);
+
+    m_ui->testInfo->hide();
+    m_ui->testProgress->hide();
+    m_ui->accountName->setFocus();
+    m_ui->checkInterval->setSuffix(ki18np(" minute", " minutes"));
+    m_ui->checkInterval->setMinimum(Akonadi::ResourceSettings::self()->minimumCheckInterval());
+    m_ui->checkInterval->setMaximum(10000);
+    m_ui->checkInterval->setSingleStep(1);
+
+    // regex for evaluating a valid server name/ip
+    mValidator.setRegExp(QRegExp(QLatin1String("[A-Za-z0-9-_:.]*")));
+    m_ui->imapServer->setValidator(&mValidator);
+
+    m_ui->folderRequester->setMimeTypeFilter(
+        QStringList() << KMime::Message::mimeType());
+    m_ui->folderRequester->setAccessRightsFilter(Akonadi::Collection::CanChangeItem | Akonadi::Collection::CanCreateItem | Akonadi::Collection::CanDeleteItem);
+    m_ui->folderRequester->changeCollectionDialogOptions(Akonadi::CollectionDialog::AllowToCreateNewChildCollection);
+    m_identityManager = new KIdentityManagement::IdentityManager(false, this, "mIdentityManager");
+    m_identityCombobox = new KIdentityManagement::IdentityCombo(m_identityManager, this);
+    m_ui->identityLabel->setBuddy(m_identityCombobox);
+    m_ui->identityLayout->addWidget(m_identityCombobox, 1);
+    m_ui->identityLabel->setBuddy(m_identityCombobox);
+
+    connect(m_ui->testButton, &QPushButton::pressed, this, &SetupServer::slotTest);
+
+    connect(m_ui->imapServer, &KLineEdit::textChanged, this, &SetupServer::slotTestChanged);
+    connect(m_ui->imapServer, &KLineEdit::textChanged, this, &SetupServer::slotComplete);
+    connect(m_ui->userName, &KLineEdit::textChanged, this, &SetupServer::slotComplete);
+    connect(m_ui->subscriptionEnabled, &QCheckBox::toggled, this, &SetupServer::slotSubcriptionCheckboxChanged);
+    connect(m_ui->subscriptionButton, &QPushButton::pressed, this, &SetupServer::slotManageSubscriptions);
+
+    connect(m_ui->managesieveCheck, &QCheckBox::toggled, this, &SetupServer::slotEnableWidgets);
+    connect(m_ui->sameConfigCheck, &QCheckBox::toggled, this, &SetupServer::slotEnableWidgets);
+
+    connect(m_ui->useDefaultIdentityCheck, &QCheckBox::toggled, this, &SetupServer::slotIdentityCheckboxChanged);
+    connect(m_ui->enableMailCheckBox, &QCheckBox::toggled, this, &SetupServer::slotMailCheckboxChanged);
+    connect(m_ui->safeImapGroup, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), this, &SetupServer::slotEncryptionRadioChanged);
+    connect(m_ui->customSieveGroup, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), this, &SetupServer::slotCustomSieveChanged);
+    connect(m_ui->showServerInfo, &QPushButton::pressed, this, &SetupServer::slotShowServerInfo);
+
+    readSettings();
+    slotTestChanged();
+    slotComplete();
+    slotCustomSieveChanged();
+    connect(networkConfigMgr, &QNetworkConfigurationManager::onlineStateChanged,
+            this, &SetupServer::slotTestChanged);
+
+    connect(mOkButton, &QPushButton::clicked, this, &SetupServer::applySettings);
+}
+
+SetupServer::~SetupServer()
+{
+    delete m_ui;
+}
+
+bool SetupServer::shouldClearCache() const
+{
+    return m_shouldClearCache;
+}
+
+void SetupServer::slotSubcriptionCheckboxChanged()
+{
+    m_ui->subscriptionButton->setEnabled(m_ui->subscriptionEnabled->isChecked());
+}
+
+void SetupServer::slotIdentityCheckboxChanged()
+{
+    m_identityCombobox->setEnabled(!m_ui->useDefaultIdentityCheck->isChecked());
+}
+
+void SetupServer::slotMailCheckboxChanged()
+{
+    m_ui->checkInterval->setEnabled(m_ui->enableMailCheckBox->isChecked());
+}
+
+void SetupServer::slotEncryptionRadioChanged()
+{
+    // TODO these really should be defined somewhere else
+    switch (m_ui->safeImapGroup->checkedId()) {
+    case KIMAP::LoginJob::Unencrypted:
+    case KIMAP::LoginJob::TlsV1:
+        m_ui->portSpin->setValue(143);
+        break;
+    case KIMAP::LoginJob::AnySslVersion:
+        m_ui->portSpin->setValue(993);
+        break;
+    default:
+        qFatal("Shouldn't happen");
+    }
+}
+
+void SetupServer::slotCustomSieveChanged()
+{
+    QAbstractButton *checkedButton = m_ui->customSieveGroup->checkedButton();
+
+    if (checkedButton == m_ui->imapUserPassword ||
+            checkedButton == m_ui->noAuthentification) {
+        m_ui->customUsername->setEnabled(false);
+        m_ui->customPassword->setEnabled(false);
+    } else if (checkedButton == m_ui->customUserPassword) {
+        m_ui->customUsername->setEnabled(true);
+        m_ui->customPassword->setEnabled(true);
+    }
+}
+
+void SetupServer::applySettings()
+{
+    m_folderArchiveSettingPage->writeSettings();
+    m_shouldClearCache = (m_parentResource->settings()->imapServer() != m_ui->imapServer->text())
+                         || (m_parentResource->settings()->userName() != m_ui->userName->text());
+
+    m_parentResource->setName(m_ui->accountName->text());
+
+    m_parentResource->settings()->setImapServer(m_ui->imapServer->text());
+    m_parentResource->settings()->setImapPort(m_ui->portSpin->value());
+    m_parentResource->settings()->setUserName(m_ui->userName->text());
+    QString encryption;
+    switch (m_ui->safeImapGroup->checkedId()) {
+    case KIMAP::LoginJob::Unencrypted :
+        encryption = QStringLiteral("None");
+        break;
+    case KIMAP::LoginJob::AnySslVersion:
+        encryption = QStringLiteral("SSL");
+        break;
+    case KIMAP::LoginJob::TlsV1:
+        encryption = QStringLiteral("STARTTLS");
+        break;
+    default:
+        qFatal("Shouldn't happen");
+    }
+    m_parentResource->settings()->setSafety(encryption);
+    MailTransport::Transport::EnumAuthenticationType::type authtype = getCurrentAuthMode(m_ui->authenticationCombo);
+    qCDebug(IMAPRESOURCE_LOG) << "saving IMAP auth mode: " << authenticationModeString(authtype);
+    m_parentResource->settings()->setAuthentication(authtype);
+    m_parentResource->settings()->setPassword(m_ui->password->text());
+    m_parentResource->settings()->setSubscriptionEnabled(m_ui->subscriptionEnabled->isChecked());
+    m_parentResource->settings()->setIntervalCheckTime(m_ui->checkInterval->value());
+    m_parentResource->settings()->setDisconnectedModeEnabled(m_ui->disconnectedModeEnabled->isChecked());
+
+    MailTransport::Transport::EnumAuthenticationType::type alternateAuthtype = getCurrentAuthMode(m_ui->authenticationAlternateCombo);
+    qCDebug(IMAPRESOURCE_LOG) << "saving Alternate server sieve auth mode: " << authenticationModeString(alternateAuthtype);
+    m_parentResource->settings()->setAlternateAuthentication(alternateAuthtype);
+    m_parentResource->settings()->setSieveSupport(m_ui->managesieveCheck->isChecked());
+    m_parentResource->settings()->setSieveReuseConfig(m_ui->sameConfigCheck->isChecked());
+    m_parentResource->settings()->setSievePort(m_ui->sievePortSpin->value());
+    m_parentResource->settings()->setSieveAlternateUrl(m_ui->alternateURL->text());
+    m_parentResource->settings()->setSieveVacationFilename(m_vacationFileName);
+
+    m_parentResource->settings()->setTrashCollection(m_ui->folderRequester->collection().id());
+    Akonadi::Collection trash = m_ui->folderRequester->collection();
+    Akonadi::SpecialMailCollections::self()->registerCollection(Akonadi::SpecialMailCollections::Trash, trash);
+    Akonadi::EntityDisplayAttribute *attribute =  trash.attribute<Akonadi::EntityDisplayAttribute>(Akonadi::Collection::AddIfMissing);
+    attribute->setIconName(QStringLiteral("user-trash"));
+    new Akonadi::CollectionModifyJob(trash);
+    if (mOldTrash != trash) {
+        Akonadi::SpecialMailCollections::self()->unregisterCollection(mOldTrash);
+    }
+
+    m_parentResource->settings()->setAutomaticExpungeEnabled(m_ui->autoExpungeCheck->isChecked());
+    m_parentResource->settings()->setUseDefaultIdentity(m_ui->useDefaultIdentityCheck->isChecked());
+
+    if (!m_ui->useDefaultIdentityCheck->isChecked()) {
+        m_parentResource->settings()->setAccountIdentity(m_identityCombobox->currentIdentity());
+    }
+
+    m_parentResource->settings()->setIntervalCheckEnabled(m_ui->enableMailCheckBox->isChecked());
+    if (m_ui->enableMailCheckBox->isChecked()) {
+        m_parentResource->settings()->setIntervalCheckTime(m_ui->checkInterval->value());
+    }
+
+    m_parentResource->settings()->setSieveCustomUsername(m_ui->customUsername->text());
+
+    QAbstractButton *checkedButton = m_ui->customSieveGroup->checkedButton();
+
+    if (checkedButton == m_ui->imapUserPassword) {
+        m_parentResource->settings()->setSieveCustomAuthentification(QStringLiteral("ImapUserPassword"));
+    } else if (checkedButton == m_ui->noAuthentification) {
+        m_parentResource->settings()->setSieveCustomAuthentification(QStringLiteral("NoAuthentification"));
+    } else if (checkedButton == m_ui->customUserPassword) {
+        m_parentResource->settings()->setSieveCustomAuthentification(QStringLiteral("CustomUserPassword"));
+    }
+
+    m_parentResource->settings()->setSieveCustomPassword(m_ui->customPassword->text());
+
+    m_parentResource->settings()->save();
+    qCDebug(IMAPRESOURCE_LOG) << "wrote" << m_ui->imapServer->text() << m_ui->userName->text() << m_ui->safeImapGroup->checkedId();
+
+    if (m_oldResourceName != m_ui->accountName->text() && !m_ui->accountName->text().isEmpty()) {
+        m_parentResource->settings()->renameRootCollection(m_ui->accountName->text());
+    }
+}
+
+void SetupServer::readSettings()
+{
+    m_folderArchiveSettingPage->loadSettings();
+    m_ui->accountName->setText(m_parentResource->name());
+    m_oldResourceName = m_ui->accountName->text();
+
+    KUser *currentUser = new KUser();
+    KEMailSettings esetting;
+
+    m_ui->imapServer->setText(
+        !m_parentResource->settings()->imapServer().isEmpty() ? m_parentResource->settings()->imapServer() :
+        esetting.getSetting(KEMailSettings::InServer));
+
+    m_ui->portSpin->setValue(m_parentResource->settings()->imapPort());
+
+    m_ui->userName->setText(
+        !m_parentResource->settings()->userName().isEmpty() ? m_parentResource->settings()->userName() :
+        currentUser->loginName());
+
+    const QString safety = m_parentResource->settings()->safety();
+    int i = 0;
+    if (safety == QLatin1String("SSL")) {
+        i = KIMAP::LoginJob::AnySslVersion;
+    } else if (safety == QLatin1String("STARTTLS")) {
+        i = KIMAP::LoginJob::TlsV1;
+    } else {
+        i = KIMAP::LoginJob::Unencrypted;
+    }
+
+    QAbstractButton *safetyButton = m_ui->safeImapGroup->button(i);
+    if (safetyButton) {
+        safetyButton->setChecked(true);
+    }
+
+    populateDefaultAuthenticationOptions();
+    i = m_parentResource->settings()->authentication();
+    qCDebug(IMAPRESOURCE_LOG) << "read IMAP auth mode: " << authenticationModeString((MailTransport::Transport::EnumAuthenticationType::type) i);
+    setCurrentAuthMode(m_ui->authenticationCombo, (MailTransport::Transport::EnumAuthenticationType::type) i);
+
+    i = m_parentResource->settings()->alternateAuthentication();
+    setCurrentAuthMode(m_ui->authenticationAlternateCombo, (MailTransport::Transport::EnumAuthenticationType::type) i);
+
+    bool rejected = false;
+    const QString password = m_parentResource->settings()->password(&rejected);
+    if (rejected) {
+        m_ui->password->setEnabled(false);
+        KMessageBox::information(Q_NULLPTR, i18n("Could not access KWallet. "
+                                 "If you want to store the password permanently then you have to "
+                                 "activate it. If you do not "
+                                 "want to use KWallet, check the box below, but note that you will be "
+                                 "prompted for your password when needed."),
+                                 i18n("Do not use KWallet"), QStringLiteral("warning_kwallet_disabled"));
+    } else {
+        m_ui->password->insert(password);
+    }
+
+    m_ui->subscriptionEnabled->setChecked(m_parentResource->settings()->subscriptionEnabled());
+
+    m_ui->checkInterval->setValue(m_parentResource->settings()->intervalCheckTime());
+    m_ui->disconnectedModeEnabled->setChecked(m_parentResource->settings()->disconnectedModeEnabled());
+
+    m_ui->managesieveCheck->setChecked(m_parentResource->settings()->sieveSupport());
+    m_ui->sameConfigCheck->setChecked(m_parentResource->settings()->sieveReuseConfig());
+    m_ui->sievePortSpin->setValue(m_parentResource->settings()->sievePort());
+    m_ui->alternateURL->setText(m_parentResource->settings()->sieveAlternateUrl());
+    m_vacationFileName = m_parentResource->settings()->sieveVacationFilename();
+
+    Akonadi::Collection trashCollection(m_parentResource->settings()->trashCollection());
+    if (trashCollection.isValid()) {
+        Akonadi::CollectionFetchJob *fetchJob = new Akonadi::CollectionFetchJob(trashCollection, Akonadi::CollectionFetchJob::Base, this);
+        connect(fetchJob, &Akonadi::CollectionFetchJob::collectionsReceived, this, &SetupServer::targetCollectionReceived);
+    } else {
+        Akonadi::SpecialMailCollectionsRequestJob *requestJob = new Akonadi::SpecialMailCollectionsRequestJob(this);
+        connect(requestJob, &Akonadi::SpecialMailCollectionsRequestJob::result, this, &SetupServer::localFolderRequestJobFinished);
+        requestJob->requestDefaultCollection(Akonadi::SpecialMailCollections::Trash);
+        requestJob->start();
+    }
+
+    m_ui->useDefaultIdentityCheck->setChecked(m_parentResource->settings()->useDefaultIdentity());
+    if (!m_ui->useDefaultIdentityCheck->isChecked()) {
+        m_identityCombobox->setCurrentIdentity(m_parentResource->settings()->accountIdentity());
+    }
+
+    m_ui->enableMailCheckBox->setChecked(m_parentResource->settings()->intervalCheckEnabled());
+    if (m_ui->enableMailCheckBox->isChecked()) {
+        m_ui->checkInterval->setValue(m_parentResource->settings()->intervalCheckTime());
+    } else {
+        m_ui->checkInterval->setEnabled(false);
+    }
+
+    m_ui->autoExpungeCheck->setChecked(m_parentResource->settings()->automaticExpungeEnabled());
+
+    if (m_vacationFileName.isEmpty()) {
+        m_vacationFileName = QStringLiteral("kmail-vacation.siv");
+    }
+
+    m_ui->customUsername->setText(m_parentResource->settings()->sieveCustomUsername());
+
+    m_ui->customPassword->setText(m_parentResource->settings()->sieveCustomPassword());
+
+    const QString sieverCustomAuth(m_parentResource->settings()->sieveCustomAuthentification());
+    if (sieverCustomAuth == QLatin1String("ImapUserPassword")) {
+        m_ui->imapUserPassword->setChecked(true);
+    } else if (sieverCustomAuth == QLatin1String("NoAuthentification")) {
+        m_ui->noAuthentification->setChecked(true);
+    } else if (sieverCustomAuth == QLatin1String("CustomUserPassword")) {
+        m_ui->customUserPassword->setChecked(true);
+    }
+
+    delete currentUser;
+}
+
+void SetupServer::slotTest()
+{
+    qCDebug(IMAPRESOURCE_LOG) << m_ui->imapServer->text();
+
+    m_ui->testButton->setEnabled(false);
+    m_ui->safeImap->setEnabled(false);
+    m_ui->authenticationCombo->setEnabled(false);
+
+    m_ui->testInfo->clear();
+    m_ui->testInfo->hide();
+
+    delete m_serverTest;
+    m_serverTest = new MailTransport::ServerTest(this);
+#ifndef QT_NO_CURSOR
+    qApp->setOverrideCursor(Qt::BusyCursor);
+#endif
+
+    const QString server = m_ui->imapServer->text();
+    const int port = m_ui->portSpin->value();
+    qCDebug(IMAPRESOURCE_LOG) << "server: " << server << "port: " << port;
+
+    m_serverTest->setServer(server);
+
+    if (port != 143 && port != 993) {
+        m_serverTest->setPort(MailTransport::Transport::EnumEncryption::None, port);
+        m_serverTest->setPort(MailTransport::Transport::EnumEncryption::SSL, port);
+    }
+
+    m_serverTest->setProtocol(QStringLiteral("imap"));
+    m_serverTest->setProgressBar(m_ui->testProgress);
+    connect(m_serverTest, &MailTransport::ServerTest::finished, this, &SetupServer::slotFinished);
+    mOkButton->setEnabled(false);
+    m_serverTest->start();
+}
+
+void SetupServer::slotFinished(const QList<int> &testResult)
+{
+    qCDebug(IMAPRESOURCE_LOG) << testResult;
+
+#ifndef QT_NO_CURSOR
+    qApp->restoreOverrideCursor();
+#endif
+    mOkButton->setEnabled(true);
+
+    using namespace MailTransport;
+
+    if (!m_serverTest->isNormalPossible() && !m_serverTest->isSecurePossible()) {
+        KMessageBox::sorry(this, i18n("Unable to connect to the server, please verify the server address."));
+    }
+
+    m_ui->testInfo->show();
+
+    m_ui->sslRadio->setEnabled(testResult.contains(Transport::EnumEncryption::SSL));
+    m_ui->tlsRadio->setEnabled(testResult.contains(Transport::EnumEncryption::TLS));
+    m_ui->noRadio->setEnabled(testResult.contains(Transport::EnumEncryption::None));
+
+    QString text;
+    if (testResult.contains(Transport::EnumEncryption::TLS)) {
+        m_ui->tlsRadio->setChecked(true);
+        text = i18n("<qt><b>TLS is supported and recommended.</b></qt>");
+    } else if (testResult.contains(Transport::EnumEncryption::SSL)) {
+        m_ui->sslRadio->setChecked(true);
+        text = i18n("<qt><b>SSL is supported and recommended.</b></qt>");
+    } else if (testResult.contains(Transport::EnumEncryption::None)) {
+        m_ui->noRadio->setChecked(true);
+        text = i18n("<qt><b>No security is supported. It is not "
+                    "recommended to connect to this server.</b></qt>");
+    } else {
+        text = i18n("<qt><b>It is not possible to use this server.</b></qt>");
+    }
+    m_ui->testInfo->setText(text);
+
+    m_ui->testButton->setEnabled(true);
+    m_ui->safeImap->setEnabled(true);
+    m_ui->authenticationCombo->setEnabled(true);
+    slotEncryptionRadioChanged();
+    slotSafetyChanged();
+}
+
+void SetupServer::slotTestChanged()
+{
+    delete m_serverTest;
+    m_serverTest = Q_NULLPTR;
+    slotSafetyChanged();
+
+    // do not use imapConnectionPossible, as the data is not yet saved.
+    m_ui->testButton->setEnabled(true /* TODO Global::connectionPossible() ||
+                                        m_ui->imapServer->text() == "localhost"*/);
+}
+
+void SetupServer::slotEnableWidgets()
+{
+    const bool haveSieve = m_ui->managesieveCheck->isChecked();
+    const bool reuseConfig = m_ui->sameConfigCheck->isChecked();
+
+    m_ui->sameConfigCheck->setEnabled(haveSieve);
+    m_ui->sievePortSpin->setEnabled(haveSieve);
+    m_ui->alternateURL->setEnabled(haveSieve && !reuseConfig);
+    m_ui->authentification->setEnabled(haveSieve && !reuseConfig);
+}
+
+void SetupServer::slotComplete()
+{
+    const bool ok =  !m_ui->imapServer->text().isEmpty() && !m_ui->userName->text().isEmpty();
+    mOkButton->setEnabled(ok);
+}
+
+void SetupServer::slotSafetyChanged()
+{
+    if (m_serverTest == Q_NULLPTR) {
+        qCDebug(IMAPRESOURCE_LOG) << "serverTest null";
+        m_ui->noRadio->setEnabled(true);
+        m_ui->sslRadio->setEnabled(true);
+        m_ui->tlsRadio->setEnabled(true);
+
+        m_ui->authenticationCombo->setEnabled(true);
+        return;
+    }
+
+    QList<int> protocols;
+
+    switch (m_ui->safeImapGroup->checkedId()) {
+    case KIMAP::LoginJob::Unencrypted :
+        qCDebug(IMAPRESOURCE_LOG) << "safeImapGroup: unencrypted";
+        protocols = m_serverTest->normalProtocols();
+        break;
+    case KIMAP::LoginJob::AnySslVersion:
+        protocols = m_serverTest->secureProtocols();
+        qCDebug(IMAPRESOURCE_LOG) << "safeImapGroup: SSL";
+        break;
+    case KIMAP::LoginJob::TlsV1:
+        protocols = m_serverTest->tlsProtocols();
+        qCDebug(IMAPRESOURCE_LOG) << "safeImapGroup: starttls";
+        break;
+    default:
+        qFatal("Shouldn't happen");
+    }
+
+    m_ui->authenticationCombo->clear();
+    addAuthenticationItem(m_ui->authenticationCombo, MailTransport::Transport::EnumAuthenticationType::CLEAR);
+    foreach (int prot, protocols) {
+        addAuthenticationItem(m_ui->authenticationCombo, (MailTransport::Transport::EnumAuthenticationType::type) prot);
+    }
+    if (protocols.size() > 0) {
+        setCurrentAuthMode(m_ui->authenticationCombo, (MailTransport::Transport::EnumAuthenticationType::type) protocols.first());
+    } else {
+        qCDebug(IMAPRESOURCE_LOG) << "no authmodes found";
+    }
+}
+
+void SetupServer::slotManageSubscriptions()
+{
+    qCDebug(IMAPRESOURCE_LOG) << "manage subscripts";
+    ImapAccount account;
+
+    account.setServer(m_ui->imapServer->text());
+    account.setPort(m_ui->portSpin->value());
+
+    account.setUserName(m_ui->userName->text());
+    account.setSubscriptionEnabled(m_ui->subscriptionEnabled->isChecked());
+
+    account.setEncryptionMode(
+        static_cast<KIMAP::LoginJob::EncryptionMode>(m_ui->safeImapGroup->checkedId())
+    );
+
+    account.setAuthenticationMode(Settings::mapTransportAuthToKimap(getCurrentAuthMode(m_ui->authenticationCombo)));
+
+    QPointer<SubscriptionDialog> subscriptions = new SubscriptionDialog(this);
+    subscriptions->setWindowTitle(i18n("Serverside Subscription"));
+    subscriptions->setWindowIcon(QIcon::fromTheme(QStringLiteral("network-server")));
+    subscriptions->connectAccount(account, m_ui->password->text());
+    m_subscriptionsChanged = subscriptions->isSubscriptionChanged();
+
+    subscriptions->exec();
+    delete subscriptions;
+
+    m_ui->subscriptionEnabled->setChecked(account.isSubscriptionEnabled());
+}
+
+void SetupServer::slotShowServerInfo()
+{
+    ServerInfoDialog *dialog = new ServerInfoDialog(m_parentResource, this);
+    dialog->show();
+}
+
+void SetupServer::targetCollectionReceived(const Akonadi::Collection::List &collections)
+{
+    m_ui->folderRequester->setCollection(collections.first());
+    mOldTrash = collections.first();
+}
+
+void SetupServer::localFolderRequestJobFinished(KJob *job)
+{
+    if (!job->error()) {
+        Akonadi::Collection targetCollection = Akonadi::SpecialMailCollections::self()->defaultCollection(Akonadi::SpecialMailCollections::Trash);
+        Q_ASSERT(targetCollection.isValid());
+        m_ui->folderRequester->setCollection(targetCollection);
+        mOldTrash = targetCollection;
+    }
+}
+
+void SetupServer::populateDefaultAuthenticationOptions()
+{
+    populateDefaultAuthenticationOptions(m_ui->authenticationCombo);
+    populateDefaultAuthenticationOptions(m_ui->authenticationAlternateCombo);
+}
+
+void SetupServer::populateDefaultAuthenticationOptions(QComboBox *combo)
+{
+    combo->clear();
+    addAuthenticationItem(combo, MailTransport::Transport::EnumAuthenticationType::CLEAR);
+    addAuthenticationItem(combo, MailTransport::Transport::EnumAuthenticationType::LOGIN);
+    addAuthenticationItem(combo, MailTransport::Transport::EnumAuthenticationType::PLAIN);
+    addAuthenticationItem(combo, MailTransport::Transport::EnumAuthenticationType::CRAM_MD5);
+    addAuthenticationItem(combo, MailTransport::Transport::EnumAuthenticationType::DIGEST_MD5);
+    addAuthenticationItem(combo, MailTransport::Transport::EnumAuthenticationType::NTLM);
+    addAuthenticationItem(combo, MailTransport::Transport::EnumAuthenticationType::GSSAPI);
+    addAuthenticationItem(combo, MailTransport::Transport::EnumAuthenticationType::ANONYMOUS);
+}
diff --git a/resources/imap/setupserver.h b/resources/imap/setupserver.h
new file mode 100644 (file)
index 0000000..a35b94e
--- /dev/null
@@ -0,0 +1,116 @@
+/* This file is part of the KDE project
+   Copyright (C) 2010 Casey Link <unnamedrambler@gmail.com>
+   Copyright (c) 2009-2010 Klaralvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
+   Copyright (C) 2009 Kevin Ottens <ervin@kde.org>
+   Copyright (C) 2006-2008 Omat Holding B.V. <info@omat.nl>
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef SETUPSERVER_H
+#define SETUPSERVER_H
+
+#include <klineedit.h>
+#include <QDialog>
+#include <collection.h>
+#include <KJob>
+
+#include <QRegExpValidator>
+class QPushButton;
+class QComboBox;
+namespace Ui
+{
+class SetupServerView;
+}
+
+namespace MailTransport
+{
+class ServerTest;
+}
+namespace KIdentityManagement
+{
+class IdentityCombo;
+class IdentityManager;
+}
+class FolderArchiveSettingPage;
+class ImapResourceBase;
+
+/**
+ * @class SetupServer
+ * These contain the account settings
+ * @author Tom Albers <tomalbers@kde.nl>
+ */
+class SetupServer : public QDialog
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Constructor
+     * @param parentResource The resource this dialog belongs to
+     * @param parent Parent WId
+     */
+    SetupServer(ImapResourceBase *parentResource, WId parent);
+
+    /**
+     * Destructor
+     */
+    ~SetupServer();
+
+    bool shouldClearCache() const;
+
+private Q_SLOTS:
+    /**
+     * Call this if you want the settings saved from this page.
+     */
+    void applySettings();
+    void slotIdentityCheckboxChanged();
+    void slotMailCheckboxChanged();
+    void slotEncryptionRadioChanged();
+    void slotSubcriptionCheckboxChanged();
+    void slotShowServerInfo();
+private:
+    void readSettings();
+    void populateDefaultAuthenticationOptions();
+
+    ImapResourceBase *m_parentResource;
+    Ui::SetupServerView *m_ui;
+    MailTransport::ServerTest *m_serverTest;
+    bool m_subscriptionsChanged;
+    bool m_shouldClearCache;
+    QString m_vacationFileName;
+    KIdentityManagement::IdentityManager *m_identityManager;
+    KIdentityManagement::IdentityCombo *m_identityCombobox;
+    QString m_oldResourceName;
+    QRegExpValidator mValidator;
+    Akonadi::Collection mOldTrash;
+    FolderArchiveSettingPage *m_folderArchiveSettingPage;
+    QPushButton *mOkButton;
+
+private Q_SLOTS:
+    void slotTest();
+    void slotFinished(const QList<int> &testResult);
+    void slotCustomSieveChanged();
+
+    void slotTestChanged();
+    void slotComplete();
+    void slotSafetyChanged();
+    void slotManageSubscriptions();
+    void slotEnableWidgets();
+    void targetCollectionReceived(const Akonadi::Collection::List &collections);
+    void localFolderRequestJobFinished(KJob *job);
+    void populateDefaultAuthenticationOptions(QComboBox *combo);
+};
+
+#endif
diff --git a/resources/imap/setupserverview_desktop.ui b/resources/imap/setupserverview_desktop.ui
new file mode 100644 (file)
index 0000000..5824647
--- /dev/null
@@ -0,0 +1,702 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SetupServerView</class>
+ <widget class="QWidget" name="SetupServerView">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>467</width>
+    <height>582</height>
+   </rect>
+  </property>
+  <layout class="QHBoxLayout" name="horizontalLayout_6">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QTabWidget" name="tabWidget">
+     <property name="currentIndex">
+      <number>0</number>
+     </property>
+     <widget class="QWidget" name="tab">
+      <attribute name="title">
+       <string>General</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_4">
+       <item>
+        <widget class="QGroupBox" name="groupBox">
+         <property name="title">
+          <string>Account Information</string>
+         </property>
+         <layout class="QFormLayout" name="formLayout_2">
+          <item row="0" column="0">
+           <widget class="QLabel" name="label_8">
+            <property name="text">
+             <string>Account Name:</string>
+            </property>
+           </widget>
+          </item>
+          <item row="0" column="1">
+           <widget class="KLineEdit" name="accountName">
+            <property name="trapReturnKey" stdset="0">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+          <item row="1" column="0">
+           <widget class="QLabel" name="label">
+            <property name="whatsThis">
+             <string>Indicate the IMAP server. If you want to connect to a non-standard port for a specific encryption scheme, you can add &quot;:port&quot; to indicate that. For example: &quot;imap.foo.com:144&quot;.</string>
+            </property>
+            <property name="text">
+             <string>I&amp;MAP Server:</string>
+            </property>
+            <property name="buddy">
+             <cstring>imapServer</cstring>
+            </property>
+           </widget>
+          </item>
+          <item row="1" column="1">
+           <widget class="KLineEdit" name="imapServer">
+            <property name="trapReturnKey" stdset="0">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+          <item row="2" column="0">
+           <widget class="QLabel" name="label_2">
+            <property name="whatsThis">
+             <string>The username.</string>
+            </property>
+            <property name="text">
+             <string>Use&amp;rname:</string>
+            </property>
+            <property name="buddy">
+             <cstring>userName</cstring>
+            </property>
+           </widget>
+          </item>
+          <item row="2" column="1">
+           <widget class="KLineEdit" name="userName">
+            <property name="trapReturnKey" stdset="0">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+          <item row="3" column="0">
+           <widget class="QLabel" name="label_3">
+            <property name="whatsThis">
+             <string>The password.</string>
+            </property>
+            <property name="text">
+             <string>Password:</string>
+            </property>
+            <property name="buddy">
+             <cstring>password</cstring>
+            </property>
+           </widget>
+          </item>
+          <item row="3" column="1">
+           <widget class="KLineEdit" name="password">
+            <property name="echoMode">
+             <enum>QLineEdit::Password</enum>
+            </property>
+            <property name="trapReturnKey" stdset="0">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <widget class="QGroupBox" name="checkOptions">
+         <property name="title">
+          <string>Mail Checking Options</string>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_6">
+          <item>
+           <widget class="QCheckBox" name="disconnectedModeEnabled">
+            <property name="text">
+             <string>&amp;Download all messages for offline use</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QCheckBox" name="enableMailCheckBox">
+            <property name="text">
+             <string>Enable &amp;interval mail checking</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <layout class="QHBoxLayout" name="horizontalLayout_1">
+            <item>
+             <widget class="QLabel" name="label_7">
+              <property name="text">
+               <string>Check mail interval:</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="KPluralHandlingSpinBox" name="checkInterval">
+              <property name="minimum">
+               <number>0</number>
+              </property>
+              <property name="value">
+               <number>1</number>
+              </property>
+             </widget>
+            </item>
+           </layout>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <spacer name="verticalSpacer_2">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>40</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tab_2">
+      <attribute name="title">
+       <string>Filtering</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_3">
+       <item>
+        <widget class="QCheckBox" name="managesieveCheck">
+         <property name="text">
+          <string>Server supports Sieve</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QCheckBox" name="sameConfigCheck">
+         <property name="text">
+          <string>Reuse host and login configuration</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLabel" name="label_14">
+         <property name="text">
+          <string>The server port changed when ManageSieve turned into a full RFC Standard. Old server implementations still use port 2000, while newer standard conform server can only be accessed via port 4190.</string>
+         </property>
+         <property name="wordWrap">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout_3">
+         <item>
+          <widget class="QLabel" name="label_5">
+           <property name="text">
+            <string>Managesieve port:</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="KPluralHandlingSpinBox" name="sievePortSpin">
+           <property name="maximum">
+            <number>65535</number>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout_4">
+         <item>
+          <widget class="QLabel" name="label_6">
+           <property name="text">
+            <string>Alternate Server:</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="KLineEdit" name="alternateURL">
+           <property name="trapReturnKey" stdset="0">
+            <bool>true</bool>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
+       <item>
+        <widget class="QGroupBox" name="authentification">
+         <property name="title">
+          <string>Authentication</string>
+         </property>
+         <layout class="QGridLayout" name="gridLayout">
+          <item row="4" column="1">
+           <widget class="KLineEdit" name="customUsername">
+            <property name="trapReturnKey" stdset="0">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+          <item row="1" column="0" colspan="2">
+           <widget class="QRadioButton" name="noAuthentification">
+            <property name="text">
+             <string>No authentication</string>
+            </property>
+            <attribute name="buttonGroup">
+             <string notr="true">customSieveGroup</string>
+            </attribute>
+           </widget>
+          </item>
+          <item row="3" column="0" colspan="2">
+           <widget class="QRadioButton" name="customUserPassword">
+            <property name="text">
+             <string>&amp;Username and Password</string>
+            </property>
+            <attribute name="buttonGroup">
+             <string notr="true">customSieveGroup</string>
+            </attribute>
+           </widget>
+          </item>
+          <item row="5" column="1">
+           <widget class="KLineEdit" name="customPassword">
+            <property name="echoMode">
+             <enum>QLineEdit::Password</enum>
+            </property>
+            <property name="trapReturnKey" stdset="0">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+          <item row="0" column="0">
+           <widget class="QRadioButton" name="imapUserPassword">
+            <property name="text">
+             <string>I&amp;MAP Username and Password</string>
+            </property>
+            <property name="checked">
+             <bool>true</bool>
+            </property>
+            <attribute name="buttonGroup">
+             <string notr="true">customSieveGroup</string>
+            </attribute>
+           </widget>
+          </item>
+          <item row="5" column="0">
+           <widget class="QLabel" name="label_13">
+            <property name="text">
+             <string>Password:</string>
+            </property>
+           </widget>
+          </item>
+          <item row="4" column="0">
+           <widget class="QLabel" name="label_12">
+            <property name="text">
+             <string>Username:</string>
+            </property>
+           </widget>
+          </item>
+          <item row="6" column="0" colspan="2">
+           <layout class="QHBoxLayout" name="horizontalLayout_2">
+            <item>
+             <widget class="QLabel" name="label_15">
+              <property name="text">
+               <string>Authentication:</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QComboBox" name="authenticationAlternateCombo">
+              <item>
+               <property name="text">
+                <string comment="Authentication method">Clear text</string>
+               </property>
+              </item>
+              <item>
+               <property name="text">
+                <string comment="Authentication method">LOGIN</string>
+               </property>
+              </item>
+              <item>
+               <property name="text">
+                <string comment="Authentication method">PLAIN</string>
+               </property>
+              </item>
+              <item>
+               <property name="text">
+                <string comment="Authentication method">CRAM-MD5</string>
+               </property>
+              </item>
+              <item>
+               <property name="text">
+                <string comment="Authentication method">DIGEST-MD5</string>
+               </property>
+              </item>
+              <item>
+               <property name="text">
+                <string comment="Authentication method">NTLM</string>
+               </property>
+              </item>
+              <item>
+               <property name="text">
+                <string comment="Authentication method">GSSAPI</string>
+               </property>
+              </item>
+              <item>
+               <property name="text">
+                <string comment="Authentication method">Anonymous</string>
+               </property>
+              </item>
+             </widget>
+            </item>
+           </layout>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <spacer name="verticalSpacer">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>40</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tab_4">
+      <attribute name="title">
+       <string>Advanced</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout">
+       <item>
+        <widget class="QGroupBox" name="groupBox_2">
+         <property name="title">
+          <string>IMAP Settings</string>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_5">
+          <item>
+           <layout class="QHBoxLayout" name="horizontalLayout_8">
+            <item>
+             <widget class="QCheckBox" name="subscriptionEnabled">
+              <property name="text">
+               <string>Enable server-side subscriptions</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="QPushButton" name="subscriptionButton">
+              <property name="enabled">
+               <bool>false</bool>
+              </property>
+              <property name="text">
+               <string>Serverside Subscription...</string>
+              </property>
+             </widget>
+            </item>
+           </layout>
+          </item>
+          <item>
+           <widget class="QCheckBox" name="autoExpungeCheck">
+            <property name="text">
+             <string>Automaticall&amp;y compact folders (expunges deleted messages)</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <layout class="QHBoxLayout" name="horizontalLayout_7">
+            <item>
+             <widget class="QLabel" name="label_11">
+              <property name="text">
+               <string>Trash folder:</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="Akonadi::CollectionRequester" name="folderRequester"/>
+            </item>
+           </layout>
+          </item>
+          <item>
+           <widget class="QGroupBox" name="identityBox">
+            <property name="title">
+             <string>Identity Settings</string>
+            </property>
+            <layout class="QVBoxLayout" name="verticalLayout_7">
+             <item>
+              <widget class="QCheckBox" name="useDefaultIdentityCheck">
+               <property name="toolTip">
+                <string>Use the default identity for this account</string>
+               </property>
+               <property name="text">
+                <string>Use &amp;default identity</string>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <layout class="QHBoxLayout" name="identityLayout">
+               <item>
+                <widget class="QLabel" name="identityLabel">
+                 <property name="toolTip">
+                  <string>Select the KMail identity used for this account</string>
+                 </property>
+                 <property name="text">
+                  <string>Identity:</string>
+                 </property>
+                </widget>
+               </item>
+              </layout>
+             </item>
+            </layout>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <widget class="QGroupBox" name="safeImap">
+         <property name="whatsThis">
+          <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'HandelGotDLig'; font-size:10pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Sans Serif'; font-weight:600;&quot;&gt;SSL/TLS&lt;/span&gt;&lt;span style=&quot; font-family:'Sans Serif';&quot;&gt; is safe IMAP over port 993;&lt;/span&gt;&lt;/p&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Sans Serif'; font-weight:600;&quot;&gt;STARTTLS&lt;/span&gt;&lt;span style=&quot; font-family:'Sans Serif';&quot;&gt; will operate on port 143 and switch to a secure connection directly after connecting;&lt;/span&gt;&lt;/p&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Sans Serif'; font-weight:600;&quot;&gt;None&lt;/span&gt;&lt;span style=&quot; font-family:'Sans Serif';&quot;&gt; will connect to port 143 but not switch to a secure connection. This setting is not recommended.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         </property>
+         <property name="title">
+          <string>Connection Settings</string>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_2">
+          <item>
+           <widget class="QPushButton" name="testButton">
+            <property name="text">
+             <string>Auto Detect</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <layout class="QFormLayout" name="formLayout">
+            <property name="fieldGrowthPolicy">
+             <enum>QFormLayout::ExpandingFieldsGrow</enum>
+            </property>
+            <item row="1" column="0">
+             <widget class="QLabel" name="label_9">
+              <property name="text">
+               <string>Encryption:</string>
+              </property>
+             </widget>
+            </item>
+            <item row="1" column="1">
+             <layout class="QHBoxLayout" name="horizontalLayout">
+              <item>
+               <widget class="QRadioButton" name="noRadio">
+                <property name="text">
+                 <string>None</string>
+                </property>
+                <property name="checked">
+                 <bool>true</bool>
+                </property>
+                <attribute name="buttonGroup">
+                 <string notr="true">safeImapGroup</string>
+                </attribute>
+               </widget>
+              </item>
+              <item>
+               <widget class="QRadioButton" name="sslRadio">
+                <property name="text">
+                 <string>SSL/TLS</string>
+                </property>
+                <attribute name="buttonGroup">
+                 <string notr="true">safeImapGroup</string>
+                </attribute>
+               </widget>
+              </item>
+              <item>
+               <widget class="QRadioButton" name="tlsRadio">
+                <property name="text">
+                 <string>STARTTLS</string>
+                </property>
+                <attribute name="buttonGroup">
+                 <string notr="true">safeImapGroup</string>
+                </attribute>
+               </widget>
+              </item>
+             </layout>
+            </item>
+            <item row="2" column="0">
+             <widget class="QLabel" name="label_4">
+              <property name="text">
+               <string>Port:</string>
+              </property>
+             </widget>
+            </item>
+            <item row="2" column="1">
+             <widget class="QSpinBox" name="portSpin">
+              <property name="minimum">
+               <number>1</number>
+              </property>
+              <property name="maximum">
+               <number>65534</number>
+              </property>
+              <property name="value">
+               <number>143</number>
+              </property>
+             </widget>
+            </item>
+            <item row="3" column="0">
+             <widget class="QLabel" name="label_10">
+              <property name="text">
+               <string>Authentication:</string>
+              </property>
+             </widget>
+            </item>
+            <item row="3" column="1">
+             <widget class="QComboBox" name="authenticationCombo">
+              <item>
+               <property name="text">
+                <string comment="Authentication method">Clear text</string>
+               </property>
+              </item>
+              <item>
+               <property name="text">
+                <string comment="Authentication method">LOGIN</string>
+               </property>
+              </item>
+              <item>
+               <property name="text">
+                <string comment="Authentication method">PLAIN</string>
+               </property>
+              </item>
+              <item>
+               <property name="text">
+                <string comment="Authentication method">CRAM-MD5</string>
+               </property>
+              </item>
+              <item>
+               <property name="text">
+                <string comment="Authentication method">DIGEST-MD5</string>
+               </property>
+              </item>
+              <item>
+               <property name="text">
+                <string comment="Authentication method">NTLM</string>
+               </property>
+              </item>
+              <item>
+               <property name="text">
+                <string comment="Authentication method">GSSAPI</string>
+               </property>
+              </item>
+              <item>
+               <property name="text">
+                <string comment="Authentication method">Anonymous</string>
+               </property>
+              </item>
+             </widget>
+            </item>
+           </layout>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLabel" name="testInfo">
+         <property name="font">
+          <font>
+           <weight>75</weight>
+           <bold>true</bold>
+          </font>
+         </property>
+         <property name="text">
+          <string>Empty</string>
+         </property>
+         <property name="alignment">
+          <set>Qt::AlignCenter</set>
+         </property>
+         <property name="wordWrap">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QProgressBar" name="testProgress">
+         <property name="value">
+          <number>24</number>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="showServerInfo">
+         <property name="text">
+          <string>Server Info</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <spacer name="verticalSpacer_3">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>40</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KLineEdit</class>
+   <extends>QLineEdit</extends>
+   <header>klineedit.h</header>
+   <container>1</container>
+  </customwidget>
+  <customwidget>
+   <class>KPluralHandlingSpinBox</class>
+   <extends>QSpinBox</extends>
+   <header>kpluralhandlingspinbox.h</header>
+  </customwidget>
+  <customwidget>
+   <class>Akonadi::CollectionRequester</class>
+   <extends>QFrame</extends>
+   <header>CollectionRequester</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+ <buttongroups>
+  <buttongroup name="customSieveGroup"/>
+  <buttongroup name="safeImapGroup"/>
+ </buttongroups>
+</ui>
diff --git a/resources/imap/subscriptiondialog.cpp b/resources/imap/subscriptiondialog.cpp
new file mode 100644 (file)
index 0000000..788e8b9
--- /dev/null
@@ -0,0 +1,396 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "subscriptiondialog.h"
+
+#include <QtCore/QCoreApplication>
+#include <QStandardItemModel>
+#include <QBoxLayout>
+#include <QKeyEvent>
+#include <QCheckBox>
+
+#include "imapresource_debug.h"
+#include <QLineEdit>
+#include <KSharedConfig>
+
+#include <KLocalizedString>
+
+#include <kimap/session.h>
+#include <kimap/loginjob.h>
+#include <kimap/unsubscribejob.h>
+#include <kimap/subscribejob.h>
+
+#include "imapaccount.h"
+#include "sessionuiproxy.h"
+#include <QPushButton>
+#include <QDialogButtonBox>
+#include <KConfigGroup>
+
+#include <QHeaderView>
+#include <QLabel>
+#include <QTreeView>
+
+SubscriptionDialog::SubscriptionDialog(QWidget *parent, SubscriptionDialog::SubscriptionDialogOptions option)
+    : QDialog(parent),
+      m_session(Q_NULLPTR),
+      m_subscriptionChanged(false),
+      m_lineEdit(Q_NULLPTR),
+      m_filter(new SubscriptionFilterProxyModel(this)),
+      m_model(new QStandardItemModel(this))
+{
+    setModal(true);
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    QVBoxLayout *topLayout = new QVBoxLayout;
+    setLayout(topLayout);
+    QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
+    okButton->setDefault(true);
+    okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
+    mUser1Button = new QPushButton;
+    buttonBox->addButton(mUser1Button, QDialogButtonBox::ActionRole);
+    connect(buttonBox, &QDialogButtonBox::accepted, this, &SubscriptionDialog::slotAccepted);
+    connect(buttonBox, &QDialogButtonBox::rejected, this, &SubscriptionDialog::reject);
+
+    mUser1Button->setText(i18nc("@action:button", "Reload &List"));
+    mUser1Button->setEnabled(false);
+    connect(mUser1Button, &QPushButton::clicked, this, &SubscriptionDialog::onReloadRequested);
+
+    QWidget *mainWidget = new QWidget(this);
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    mainWidget->setLayout(mainLayout);
+    topLayout->addWidget(mainWidget);
+    topLayout->addWidget(buttonBox);
+
+    m_enableSubscription = new QCheckBox(i18nc("@option:check",
+                                         "Enable server-side subscriptions"));
+    mainLayout->addWidget(m_enableSubscription);
+
+    QHBoxLayout *filterBarLayout = new QHBoxLayout;
+    mainLayout->addLayout(filterBarLayout);
+
+    filterBarLayout->addWidget(new QLabel(i18nc("@label search for a subscription",
+                                          "Search:")));
+
+    m_lineEdit = new QLineEdit(mainWidget);
+    m_lineEdit->setClearButtonEnabled(true);
+    connect(m_lineEdit, &QLineEdit::textChanged, this, &SubscriptionDialog::slotSearchPattern);
+    filterBarLayout->addWidget(m_lineEdit);
+    m_lineEdit->setFocus();
+
+    QCheckBox *checkBox = new QCheckBox(i18nc("@option:check", "Subscribed only"), mainWidget);
+    connect(checkBox, SIGNAL(stateChanged(int)),
+            m_filter, SLOT(setIncludeCheckedOnly(int)));
+
+    filterBarLayout->addWidget(checkBox);
+
+    m_treeView = new QTreeView(mainWidget);
+    m_treeView->header()->hide();
+    m_filter->setSourceModel(m_model);
+    m_treeView->setModel(m_filter);
+    mainLayout->addWidget(m_treeView);
+
+    connect(m_model, &QStandardItemModel::itemChanged, this, &SubscriptionDialog::onItemChanged);
+
+    if (option & SubscriptionDialog::AllowToEnableSubscription) {
+        connect(m_enableSubscription, &QCheckBox::clicked, m_treeView, &QTreeView::setEnabled);
+    } else {
+        m_enableSubscription->hide();
+    }
+    readConfig();
+}
+
+SubscriptionDialog::~SubscriptionDialog()
+{
+    writeConfig();
+}
+
+void SubscriptionDialog::slotSearchPattern(const QString &pattern)
+{
+    m_treeView->expandAll();
+    m_filter->setSearchPattern(pattern);
+}
+
+void SubscriptionDialog::readConfig()
+{
+    KConfigGroup group(KSharedConfig::openConfig(), "SubscriptionDialog");
+
+    const QSize size = group.readEntry("Size", QSize());
+    if (size.isValid()) {
+        resize(size);
+    } else {
+        resize(500, 300);
+    }
+}
+
+void SubscriptionDialog::writeConfig()
+{
+    KConfigGroup group(KSharedConfig::openConfig(), "SubscriptionDialog");
+    group.writeEntry("Size", size());
+    group.sync();
+}
+
+void SubscriptionDialog::setSubscriptionEnabled(bool enabled)
+{
+    m_enableSubscription->setChecked(enabled);
+    m_treeView->setEnabled(enabled);
+}
+
+bool SubscriptionDialog::subscriptionEnabled() const
+{
+    return m_enableSubscription->isChecked();
+}
+
+void SubscriptionDialog::connectAccount(const ImapAccount &account, const QString &password)
+{
+    m_session = new KIMAP::Session(account.server(), account.port(), this);
+    m_session->setUiProxy(SessionUiProxy::Ptr(new SessionUiProxy));
+
+    KIMAP::LoginJob *login = new KIMAP::LoginJob(m_session);
+    login->setUserName(account.userName());
+    login->setPassword(password);
+    login->setEncryptionMode(account.encryptionMode());
+    login->setAuthenticationMode(account.authenticationMode());
+
+    connect(login, &KIMAP::LoginJob::result, this, &SubscriptionDialog::onLoginDone);
+    login->start();
+}
+
+bool SubscriptionDialog::isSubscriptionChanged() const
+{
+    return m_subscriptionChanged;
+}
+
+void SubscriptionDialog::onLoginDone(KJob *job)
+{
+    if (!job->error()) {
+        onReloadRequested();
+    }
+}
+
+void SubscriptionDialog::onReloadRequested()
+{
+    mUser1Button->setEnabled(false);
+    m_itemsMap.clear();
+    m_model->clear();
+
+    // we need a connection
+    if (!m_session
+            || m_session->state() != KIMAP::Session::Authenticated) {
+        qCWarning(IMAPRESOURCE_LOG) << "SubscriptionDialog - got no connection";
+        mUser1Button->setEnabled(true);
+        return;
+    }
+
+    KIMAP::ListJob *list = new KIMAP::ListJob(m_session);
+    list->setIncludeUnsubscribed(true);
+    connect(list, &KIMAP::ListJob::mailBoxesReceived, this, &SubscriptionDialog::onMailBoxesReceived);
+    connect(list, &KIMAP::ListJob::result, this, &SubscriptionDialog::onFullListingDone);
+    list->start();
+}
+
+void SubscriptionDialog::onMailBoxesReceived(const QList<KIMAP::MailBoxDescriptor> &mailBoxes,
+        const QList< QList<QByteArray> > &flags)
+{
+    const int numberOfMailBoxes(mailBoxes.size());
+    for (int i = 0; i < numberOfMailBoxes; i++) {
+        KIMAP::MailBoxDescriptor mailBox = mailBoxes[i];
+
+        const QStringList pathParts = mailBox.name.split(mailBox.separator);
+        const QString separator = mailBox.separator;
+        Q_ASSERT(separator.size() == 1);   // that's what the spec says
+
+        QString parentPath;
+        QString currentPath;
+        const int numberOfPath(pathParts.size());
+        for (int j = 0; j < pathParts.size(); ++j) {
+            const bool isDummy = (j != (numberOfPath - 1));
+            const bool isCheckable = !isDummy && !flags[i].contains("\\noselect");
+
+            const QString pathPart = pathParts.at(j);
+            currentPath += separator + pathPart;
+
+            if (m_itemsMap.contains(currentPath)) {
+                if (!isDummy) {
+                    QStandardItem *item = m_itemsMap[currentPath];
+                    item->setCheckable(isCheckable);
+                }
+
+            } else if (!parentPath.isEmpty()) {
+                Q_ASSERT(m_itemsMap.contains(parentPath));
+
+                QStandardItem *parentItem = m_itemsMap[parentPath];
+
+                QStandardItem *item = new QStandardItem(pathPart);
+                item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
+                item->setCheckable(isCheckable);
+                item->setData(currentPath.mid(1), PathRole);
+                parentItem->appendRow(item);
+                m_itemsMap[currentPath] = item;
+
+            } else {
+                QStandardItem *item = new QStandardItem(pathPart);
+                item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
+                item->setCheckable(isCheckable);
+                item->setData(currentPath.mid(1), PathRole);
+                m_model->appendRow(item);
+                m_itemsMap[currentPath] = item;
+            }
+
+            parentPath = currentPath;
+        }
+    }
+}
+
+void SubscriptionDialog::onFullListingDone(KJob *job)
+{
+    if (job->error()) {
+        mUser1Button->setEnabled(true);
+        return;
+    }
+
+    KIMAP::ListJob *list = new KIMAP::ListJob(m_session);
+    list->setIncludeUnsubscribed(false);
+    connect(list, &KIMAP::ListJob::mailBoxesReceived, this, &SubscriptionDialog::onSubscribedMailBoxesReceived);
+    connect(list, &KIMAP::ListJob::result, this, &SubscriptionDialog::onReloadDone);
+    list->start();
+}
+
+void SubscriptionDialog::onSubscribedMailBoxesReceived(const QList<KIMAP::MailBoxDescriptor> &mailBoxes,
+        const QList< QList<QByteArray> > &flags)
+{
+    Q_UNUSED(flags);
+    const int numberOfMailBoxes(mailBoxes.size());
+    for (int i = 0; i < numberOfMailBoxes; ++i) {
+        KIMAP::MailBoxDescriptor mailBox = mailBoxes.at(i);
+        QString descriptor = mailBox.separator + mailBox.name;
+
+        if (m_itemsMap.contains(descriptor)) {
+            QStandardItem *item = m_itemsMap[mailBox.separator + mailBox.name];
+            item->setCheckState(Qt::Checked);
+            item->setData(Qt::Checked, InitialStateRole);
+        }
+    }
+}
+
+void SubscriptionDialog::onReloadDone(KJob *job)
+{
+    Q_UNUSED(job);
+    mUser1Button->setEnabled(true);
+}
+
+void SubscriptionDialog::onItemChanged(QStandardItem *item)
+{
+    QFont font = item->font();
+    font.setBold(item->checkState() != item->data(InitialStateRole).toInt());
+    item->setFont(font);
+}
+
+void SubscriptionDialog::slotAccepted()
+{
+    applyChanges();
+    accept();
+}
+
+void SubscriptionDialog::applyChanges()
+{
+    QList<QStandardItem *> items = m_itemsMap.values();
+
+    while (!items.isEmpty()) {
+        QStandardItem *item = items.takeFirst();
+
+        if (item->checkState() != item->data(InitialStateRole).toInt()) {
+            if (item->checkState() == Qt::Checked) {
+                qCDebug(IMAPRESOURCE_LOG) << "Subscribing" << item->data(PathRole);
+                KIMAP::SubscribeJob *subscribe = new KIMAP::SubscribeJob(m_session);
+                subscribe->setMailBox(item->data(PathRole).toString());
+                subscribe->exec();
+            } else {
+                qCDebug(IMAPRESOURCE_LOG) << "Unsubscribing" << item->data(PathRole);
+                KIMAP::UnsubscribeJob *unsubscribe = new KIMAP::UnsubscribeJob(m_session);
+                unsubscribe->setMailBox(item->data(PathRole).toString());
+                unsubscribe->exec();
+            }
+
+            m_subscriptionChanged = true;
+        }
+    }
+}
+
+SubscriptionFilterProxyModel::SubscriptionFilterProxyModel(QObject *parent)
+    : KRecursiveFilterProxyModel(parent), m_checkedOnly(false)
+{
+
+}
+
+void SubscriptionFilterProxyModel::setSearchPattern(const QString &pattern)
+{
+    if (m_pattern != pattern) {
+        m_pattern = pattern;
+        invalidate();
+    }
+}
+
+void SubscriptionFilterProxyModel::setIncludeCheckedOnly(bool checkedOnly)
+{
+    if (m_checkedOnly != checkedOnly) {
+        m_checkedOnly = checkedOnly;
+        invalidate();
+    }
+}
+
+void SubscriptionFilterProxyModel::setIncludeCheckedOnly(int checkedOnlyState)
+{
+    m_checkedOnly = (checkedOnlyState == Qt::Checked);
+    invalidate();
+}
+
+bool SubscriptionFilterProxyModel::acceptRow(int sourceRow, const QModelIndex &sourceParent) const
+{
+    QModelIndex sourceIndex = sourceModel()->index(sourceRow, 0, sourceParent);
+
+    const bool checked = sourceIndex.data(Qt::CheckStateRole).toInt() == Qt::Checked;
+
+    if (m_checkedOnly && !checked) {
+        return false;
+    } else if (!m_pattern.isEmpty()) {
+        const QString text = sourceIndex.data(Qt::DisplayRole).toString();
+        return text.contains(m_pattern, Qt::CaseInsensitive);
+    } else {
+        return true;
+    }
+}
+
+void SubscriptionDialog::onMobileLineEditChanged(const QString &text)
+{
+    if (!text.isEmpty() && !m_lineEdit->isVisible()) {
+        m_lineEdit->show();
+        m_lineEdit->setFocus();
+        m_lineEdit->grabKeyboard(); // Now the line edit runs the show
+    } else if (text.isEmpty() && m_lineEdit->isVisible()) {
+        m_lineEdit->hide();
+        grabKeyboard(); // Line edit gone, so let's get all the events for us again
+    }
+}
+
+void SubscriptionDialog::keyPressEvent(QKeyEvent *event)
+{
+    QDialog::keyPressEvent(event);
+}
+
diff --git a/resources/imap/subscriptiondialog.h b/resources/imap/subscriptiondialog.h
new file mode 100644 (file)
index 0000000..b950f30
--- /dev/null
@@ -0,0 +1,120 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef SUBSCRIPTIONDIALOG_H
+#define SUBSCRIPTIONDIALOG_H
+
+#include <QDialog>
+
+#include <krecursivefilterproxymodel.h>
+#include <kimap/listjob.h>
+
+#include <QtCore/QMap>
+
+class QKeyEvent;
+class QStandardItemModel;
+class QStandardItem;
+
+class QLineEdit;
+class QCheckBox;
+class ImapAccount;
+class QTreeView;
+class QListView;
+class QPushButton;
+
+class SubscriptionFilterProxyModel : public KRecursiveFilterProxyModel
+{
+    Q_OBJECT
+public:
+    explicit SubscriptionFilterProxyModel(QObject *parent = Q_NULLPTR);
+
+public Q_SLOTS:
+    void setSearchPattern(const QString &pattern);
+    void setIncludeCheckedOnly(bool checkedOnly);
+    void setIncludeCheckedOnly(int checkedOnlyState);
+
+protected:
+    bool acceptRow(int sourceRow, const QModelIndex &sourceParent) const Q_DECL_OVERRIDE;
+
+private:
+    QString m_pattern;
+    bool m_checkedOnly;
+};
+
+class SubscriptionDialog : public QDialog
+{
+    Q_OBJECT
+public:
+    enum Roles {
+        InitialStateRole = Qt::UserRole + 1,
+        PathRole
+    };
+    enum SubscriptionDialogOption {
+        None = 0,
+        AllowToEnableSubscription = 1
+    };
+    Q_DECLARE_FLAGS(SubscriptionDialogOptions, SubscriptionDialogOption)
+
+    explicit SubscriptionDialog(QWidget *parent = Q_NULLPTR, SubscriptionDialog::SubscriptionDialogOptions option = SubscriptionDialog::None);
+    ~SubscriptionDialog();
+
+    void connectAccount(const ImapAccount &account, const QString &password);
+    bool isSubscriptionChanged() const;
+    void setSubscriptionEnabled(bool enabled);
+    bool subscriptionEnabled() const;
+
+private Q_SLOTS:
+    void onLoginDone(KJob *job);
+    void onReloadRequested();
+    void onMailBoxesReceived(const QList<KIMAP::MailBoxDescriptor> &mailBoxes,
+                             const QList< QList<QByteArray> > &flags);
+    void onFullListingDone(KJob *job);
+    void onSubscribedMailBoxesReceived(const QList<KIMAP::MailBoxDescriptor> &mailBoxes,
+                                       const QList< QList<QByteArray> > &flags);
+    void onReloadDone(KJob *job);
+    void onItemChanged(QStandardItem *item);
+    void onMobileLineEditChanged(const QString &text);
+
+    void slotSearchPattern(const QString &pattern);
+protected:
+    void keyPressEvent(QKeyEvent *event) Q_DECL_OVERRIDE;
+
+protected Q_SLOTS:
+    void slotAccepted();
+private:
+    void readConfig();
+    void writeConfig();
+    void applyChanges();
+
+    KIMAP::Session *m_session;
+    bool m_subscriptionChanged;
+
+    QTreeView *m_treeView;
+
+    QLineEdit *m_lineEdit;
+    QCheckBox *m_enableSubscription;
+    SubscriptionFilterProxyModel *m_filter;
+    QStandardItemModel *m_model;
+    QMap<QString, QStandardItem *> m_itemsMap;
+    QPushButton *mUser1Button;
+};
+
+#endif
diff --git a/resources/imap/tests/CMakeLists.txt b/resources/imap/tests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..b1c29e7
--- /dev/null
@@ -0,0 +1,15 @@
+kde_enable_exceptions()
+set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR})
+
+set(testsubscriptiondialog_SRCS
+  testsubscriptiondialog.cpp
+  ../imapaccount.cpp
+  ../subscriptiondialog.cpp
+  ../imapresource_debug.cpp
+)
+
+include_directories(${CMAKE_CURRENT_SOURCE_DIR}/.. ${CMAKE_CURRENT_BINARY_DIR}/..)
+add_executable(testsubscriptiondialog ${testsubscriptiondialog_SRCS})
+
+target_link_libraries(testsubscriptiondialog KF5::KIOCore KF5::IMAP KF5::Mime KF5::ItemModels Qt5::Widgets KF5::I18n KF5::KIOWidgets KF5::AkonadiCore KF5::MailTransport)
+
diff --git a/resources/imap/tests/testsubscriptiondialog.cpp b/resources/imap/tests/testsubscriptiondialog.cpp
new file mode 100644 (file)
index 0000000..387c77c
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include <QtCore/QDebug>
+#include <QtCore/QStringList>
+#include <QApplication>
+
+#include "imapaccount.h"
+#include "subscriptiondialog.h"
+
+int main(int argc, char **argv)
+{
+    QApplication app(argc, argv);
+
+    if (app.arguments().size() < 5) {
+        qWarning("Not enough parameters, expecting: <server> <port> <user> <password>");
+        return 1;
+    }
+
+    QString server = app.arguments().at(1);
+    int port = app.arguments().at(2).toInt();
+    QString user = app.arguments().at(3);
+    QString password = app.arguments().at(4);
+
+    qDebug() << "Querying:" << server << port << user << password;
+    qDebug();
+
+    ImapAccount account;
+    account.setServer(server);
+    account.setPort(port);
+    account.setUserName(user);
+
+    SubscriptionDialog *dialog = new SubscriptionDialog();
+    dialog->connectAccount(account, password);
+
+    dialog->show();
+
+    int retcode = app.exec();
+
+    qDebug() << "Subscription changed?" << dialog->isSubscriptionChanged();
+
+    return retcode;
+}
diff --git a/resources/imap/tracer.cpp b/resources/imap/tracer.cpp
new file mode 100644 (file)
index 0000000..9e6bf61
--- /dev/null
@@ -0,0 +1,61 @@
+#include "tracer.h"
+#include "imapresource_debug.h"
+
+#include <QString>
+#include <QIODevice>
+#include <QCoreApplication>
+#include <iostream>
+#include <unistd.h>
+
+class DebugStream: public QIODevice
+{
+public:
+    QString m_location;
+    DebugStream()
+        : QIODevice()
+    {
+        open(WriteOnly);
+    }
+    virtual ~DebugStream() {};
+
+    bool isSequential() const Q_DECL_OVERRIDE
+    {
+        return true;
+    }
+    qint64 readData(char *, qint64) Q_DECL_OVERRIDE {
+        return 0; /* eof */
+    }
+    qint64 readLineData(char *, qint64) Q_DECL_OVERRIDE {
+        return 0; /* eof */
+    }
+    qint64 writeData(const char *data, qint64 len) Q_DECL_OVERRIDE {
+        const QByteArray buf = QByteArray::fromRawData(data, len);
+        if (!qEnvironmentVariableIsEmpty("IMAP_TRACE"))
+        {
+            // qt_message_output(QtDebugMsg, buf.trimmed().constData());
+            std::cout << buf.trimmed().constData() << std::endl;
+        }
+        return len;
+    }
+private:
+    Q_DISABLE_COPY(DebugStream)
+};
+
+QDebug debugStream(int line, const char *file, const char *function)
+{
+    static DebugStream stream;
+    QDebug debug(&stream);
+
+    static QByteArray programName;
+    if (programName.isEmpty()) {
+        if (QCoreApplication::instance()) {
+            programName = QCoreApplication::instance()->applicationName().toLocal8Bit();
+        } else {
+            programName = "<unknown program name>";
+        }
+    }
+
+    debug << QStringLiteral("Trace:%1(%2) %3:").arg(QString::fromLatin1(programName)).arg(unsigned(getpid())).arg(QLatin1String(function)) /* << file << ":" << line */;
+
+    return debug;
+}
diff --git a/resources/imap/tracer.h b/resources/imap/tracer.h
new file mode 100644 (file)
index 0000000..5863020
--- /dev/null
@@ -0,0 +1,7 @@
+#pragma once
+
+#include "imapresource_debug.h"
+
+QDebug debugStream(int line, const char *file, const char *function);
+
+#define Trace() debugStream(__LINE__, __FILE__, Q_FUNC_INFO)
diff --git a/resources/imap/uidnextattribute.cpp b/resources/imap/uidnextattribute.cpp
new file mode 100644 (file)
index 0000000..ce0177e
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+    Copyright (C) 2008 Omat Holding B.V. <info@omat.nl>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "uidnextattribute.h"
+
+#include <QByteArray>
+
+#include <attribute.h>
+
+UidNextAttribute::UidNextAttribute(int uidnext)
+    : mUidNext(uidnext)
+{
+}
+
+void UidNextAttribute::setUidNext(int uidnext)
+{
+    mUidNext = uidnext;
+}
+
+int UidNextAttribute::uidNext() const
+{
+    return mUidNext;
+}
+
+QByteArray UidNextAttribute::type() const
+{
+    static const QByteArray sType("uidnext");
+    return sType;
+}
+
+Akonadi::Attribute *UidNextAttribute::clone() const
+{
+    return new UidNextAttribute(mUidNext);
+}
+
+QByteArray UidNextAttribute::serialized() const
+{
+    return QByteArray::number(mUidNext);
+}
+
+void UidNextAttribute::deserialize(const QByteArray &data)
+{
+    mUidNext = data.toInt();
+}
diff --git a/resources/imap/uidnextattribute.h b/resources/imap/uidnextattribute.h
new file mode 100644 (file)
index 0000000..8d72567
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+    Copyright (C) 2008 Omat Holding B.V. <info@omat.nl>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef UIDNEXTATTRIBUTE_H
+#define UIDNEXTATTRIBUTE_H
+
+#include <attribute.h>
+
+class UidNextAttribute : public Akonadi::Attribute
+{
+public:
+    explicit UidNextAttribute(int uidnext = 0);
+    void setUidNext(int uidnext);
+    int uidNext() const;
+    QByteArray type() const Q_DECL_OVERRIDE;
+    Attribute *clone() const Q_DECL_OVERRIDE;
+    QByteArray serialized() const Q_DECL_OVERRIDE;
+    void deserialize(const QByteArray &data) Q_DECL_OVERRIDE;
+
+private:
+    int mUidNext;
+};
+
+#endif
diff --git a/resources/imap/uidvalidityattribute.cpp b/resources/imap/uidvalidityattribute.cpp
new file mode 100644 (file)
index 0000000..6365140
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+    Copyright (C) 2008 Omat Holding B.V. <info@omat.nl>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "uidvalidityattribute.h"
+
+#include <QByteArray>
+
+#include <attribute.h>
+
+UidValidityAttribute::UidValidityAttribute(int uidvalidity)
+    : mUidValidity(uidvalidity)
+{
+}
+
+void UidValidityAttribute::setUidValidity(int uidvalidity)
+{
+    mUidValidity = uidvalidity;
+}
+
+int UidValidityAttribute::uidValidity() const
+{
+    return mUidValidity;
+}
+
+QByteArray UidValidityAttribute::type() const
+{
+    static const QByteArray sType("uidvalidity");
+    return sType;
+}
+
+Akonadi::Attribute *UidValidityAttribute::clone() const
+{
+    return new UidValidityAttribute(mUidValidity);
+}
+
+QByteArray UidValidityAttribute::serialized() const
+{
+    return QByteArray::number(mUidValidity);
+}
+
+void UidValidityAttribute::deserialize(const QByteArray &data)
+{
+    mUidValidity = data.toInt();
+}
diff --git a/resources/imap/uidvalidityattribute.h b/resources/imap/uidvalidityattribute.h
new file mode 100644 (file)
index 0000000..a12c60a
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+    Copyright (C) 2008 Omat Holding B.V. <info@omat.nl>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef UIDVALIDITYATTRIBUTE_H
+#define UIDVALIDITYATTRIBUTE_H
+
+#include <attribute.h>
+
+class UidValidityAttribute : public Akonadi::Attribute
+{
+public:
+    explicit UidValidityAttribute(int uidvalidity = 0);
+    void setUidValidity(int uidvalidity);
+    int uidValidity() const;
+    QByteArray type() const Q_DECL_OVERRIDE;
+    Attribute *clone() const Q_DECL_OVERRIDE;
+    QByteArray serialized() const Q_DECL_OVERRIDE;
+    void deserialize(const QByteArray &data) Q_DECL_OVERRIDE;
+
+private:
+    int mUidValidity;
+};
+
+#endif
diff --git a/resources/imap/wizard/CMakeLists.txt b/resources/imap/wizard/CMakeLists.txt
new file mode 100644 (file)
index 0000000..f504289
--- /dev/null
@@ -0,0 +1,2 @@
+
+install ( FILES imapwizard.desktop imapwizard.es imapwizard.ui DESTINATION ${KDE_INSTALL_DATADIR}/akonadi/accountwizard/imap )
diff --git a/resources/imap/wizard/Messages.sh b/resources/imap/wizard/Messages.sh
new file mode 100755 (executable)
index 0000000..adcf81e
--- /dev/null
@@ -0,0 +1,4 @@
+#! /usr/bin/env bash
+$EXTRACTRC *.ui >> rc.cpp
+$XGETTEXT *.cpp -o $podir/accountwizard_imap.pot
+$XGETTEXT -kqsTr *.es -j -o $podir/accountwizard_imap.pot
diff --git a/resources/imap/wizard/imapwizard.desktop b/resources/imap/wizard/imapwizard.desktop
new file mode 100644 (file)
index 0000000..62ea3b2
--- /dev/null
@@ -0,0 +1,101 @@
+[Desktop Entry]
+Name=Generic IMAP Email Server
+Name[bs]=Generički IMAP Email Server
+Name[ca]=Servidor de correu IMAP genèric
+Name[ca@valencia]=Servidor de correu IMAP genèric
+Name[cs]=Obecný e-mailový IMAP server
+Name[da]=Generisk IMAP e-mail-server
+Name[de]=Allgemeiner IMAP-E-Mail-Server
+Name[el]=Γενικός εξυπηρετητής ηλ.ταχυδρομείου IMAP
+Name[en_GB]=Generic IMAP Email Server
+Name[es]=Servidor de correo IMAP genérico
+Name[et]=Üldine IMAP e-posti server
+Name[fi]=Yleinen IMAP-sähköpostipalvelin
+Name[fr]=Serveur générique de courriers électroniques IMAP
+Name[ga]=Freastalaí Ríomhphoist IMAP Ginearálta
+Name[gl]=Servidor de correo IMAP xenérico
+Name[hu]=Általános IMAP e-mail kiszolgáló
+Name[ia]=Servitor generic de e-posta IMAP
+Name[it]=Server di posta IMAP generico
+Name[ja]=汎用 IMAP メールサーバ
+Name[kk]=Жалпы IMAP эл.пошта сервері
+Name[km]=ម៉ាស៊ីន​បម្រើ​អ៊ីមែល IMAP ទូទៅ
+Name[ko]=일반 IMAP 이메일 서버
+Name[lt]=Pagrindinis IMAP pašto serveris
+Name[lv]=Vispārīgs IMAP e-pasta serveris
+Name[nb]=Generisk IMAP E-post-tjener
+Name[nds]=Allgemeen IMAP-Nettpostserver
+Name[nl]=Generieke IMAP e-mailserver
+Name[pa]=ਆਮ IMAP ਈਮੇਲ ਸਰਵਰ
+Name[pl]=Zwykły serwer poczty IMAP
+Name[pt]=Servidor Genérico de E-Mail IMAP
+Name[pt_BR]=Servidor de e-mails IMAP genérico
+Name[ro]=Server de poștă IMAP generic
+Name[ru]=Почтовый сервер IMAP
+Name[sk]=Všeobecný IMAP Email Server
+Name[sl]=Splošni e-poštni strežnik IMAP
+Name[sr]=Генерички ИМАП сервер е‑поште
+Name[sr@ijekavian]=Генерички ИМАП сервер е‑поште
+Name[sr@ijekavianlatin]=Generički IMAP server e‑pošte
+Name[sr@latin]=Generički IMAP server e‑pošte
+Name[sv]=Generell IMAP e-postserver
+Name[tr]=Genel IMAP E-posta Sunucu
+Name[uk]=Типовий сервер пошти IMAP
+Name[x-test]=xxGeneric IMAP Email Serverxx
+Name[zh_CN]=普通 IMAP 电子邮件服务器
+Name[zh_TW]=一般 IMAP 信件伺服器
+Icon=network-server
+Comment=Imap account
+Comment[bg]=Сметка IMAP
+Comment[bs]=Imap nalog
+Comment[ca]=Compte Imap
+Comment[ca@valencia]=Compte Imap
+Comment[cs]=Účet IMAP
+Comment[da]=IMAP-konto
+Comment[de]=IMAP-Zugang
+Comment[el]=Λογαριασμός imap
+Comment[en_GB]=Imap account
+Comment[es]=Cuenta Imap
+Comment[et]=IMAP konto
+Comment[fi]=IMAP-tili
+Comment[fr]=Compte IMAP
+Comment[ga]=Cuntas IMAP
+Comment[gl]=Conta de IMAP
+Comment[hu]=IMAP azonosító
+Comment[ia]=Conto de IMAP
+Comment[it]=Account IMAP
+Comment[ja]=Imap アカウント
+Comment[kk]=Imap тіркелгісі
+Comment[km]=គណនី Imap
+Comment[ko]=IMAP 계정
+Comment[lt]=Imap paskyra
+Comment[lv]=IMAP konts
+Comment[nb]=Imap-konto
+Comment[nds]=Imap-Konto
+Comment[nl]=Imap-account
+Comment[nn]=IMAP-konto
+Comment[pa]=Imap ਅਕਾਊਂਟ
+Comment[pl]=Konto Imap
+Comment[pt]=Conta de IMAP
+Comment[pt_BR]=Conta IMAP
+Comment[ro]=Cont Imap
+Comment[ru]=Учётная запись imap
+Comment[sk]=Imap účet
+Comment[sl]=Račun IMAP
+Comment[sr]=ИМАП налог
+Comment[sr@ijekavian]=ИМАП налог
+Comment[sr@ijekavianlatin]=IMAP nalog
+Comment[sr@latin]=IMAP nalog
+Comment[sv]=Imap-konto
+Comment[tr]=Imap hesabı
+Comment[uk]=Обліковий запис IMAP
+Comment[x-test]=xxImap accountxx
+Comment[zh_CN]=IMAP 账户
+Comment[zh_TW]=Imap 帳號
+
+[Wizard]
+Type=message/rfc822
+Script=imapwizard.es
+
+[Translate]
+Filename=accountwizard_imap
diff --git a/resources/imap/wizard/imapwizard.es b/resources/imap/wizard/imapwizard.es
new file mode 100644 (file)
index 0000000..3e4dd58
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+    Copyright (c) 2009 Montel Laurent <montel@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+// add this function to trim user input of whitespace when needed
+String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ""); };
+
+var page = Dialog.addPage( "imapwizard.ui", qsTr("Personal Settings") );
+
+// try to guess some defaults
+var emailAddr = SetupManager.email();
+var pos = emailAddr.indexOf( "@" );
+if ( pos >= 0 && (pos + 1) < emailAddr.length ) {
+  var server = emailAddr.slice( pos + 1, emailAddr.length );
+  page.widget().incommingAddress.text = server;
+  page.widget().outgoingAddress.text = server;
+  // We must not strip the server from the user identifier.
+  // Otherwise the 'user' will be kdabtest1 instead of kdabtest1@demo.kolab.org
+  // which fails,
+  //var user = emailAddr.slice( 0, pos );
+  page.widget().userName.text = emailAddr;
+}
+
+function validateInput()
+{
+  if ( page.widget().incommingAddress.text.trim() == "" ) {
+    page.setValid( false );
+  } else {
+    page.setValid( true );
+  }
+}
+
+var stage = 1;
+var identity;
+
+function setup()
+{
+  if ( stage == 1 ) {
+    identity = SetupManager.createIdentity();
+    identity.setEmail( SetupManager.email() );
+    identity.setRealName( SetupManager.name() );
+
+    ServerTest.test( page.widget().incommingAddress.text.trim(), "imap" );
+  } else {
+    ServerTest.test( page.widget().outgoingAddress.text.trim(), "smtp" );
+  }
+}
+
+function testResultFail()
+{
+  testOk( -1 );
+}
+
+function testOk( arg )
+{
+  if (stage == 1) {
+    SetupManager.openWallet();
+    var imapRes = SetupManager.createResource( "akonadi_imap_resource" );
+    imapRes.setOption( "ImapServer", page.widget().incommingAddress.text.trim() );
+    imapRes.setOption( "UserName", page.widget().userName.text );
+    imapRes.setOption( "Password", SetupManager.password() );
+    imapRes.setOption( "DisconnectedModeEnabled", page.widget().disconnectedMode.checked );
+    imapRes.setOption( "UseDefaultIdentity", false );
+    imapRes.setOption( "AccountIdentity", identity.uoid() );
+    imapRes.setOption( "Authentication", 7 ); // ClearText
+    if ( arg == "ssl" ) { 
+      // The ENUM used for authentication (in the imap resource only)
+      imapRes.setOption( "Safety", "SSL"); // SSL/TLS
+      imapRes.setOption( "ImapPort", 993 );
+    } else if ( arg == "tls" ) { // tls is really STARTTLS
+      imapRes.setOption( "Safety", "STARTTLS");  // STARTTLS
+      imapRes.setOption( "ImapPort", 143 );
+    } else if ( arg == "none" ) {
+      imapRes.setOption( "Safety", "NONE" );  // No encryption
+      imapRes.setOption( "ImapPort", 143 );
+    } else {
+      // safe default fallback when servertest failed
+      imapRes.setOption( "Safety", "STARTTLS");
+      imapRes.setOption( "ImapPort", 143 );
+    }
+    imapRes.setOption( "IntervalCheckTime", 60 );
+    imapRes.setOption( "SubscriptionEnabled", true );
+
+    stage = 2;
+    setup();
+  } else {
+    var smtp = SetupManager.createTransport( "smtp" );
+    smtp.setName( page.widget().outgoingAddress.text.trim() );
+    smtp.setHost( page.widget().outgoingAddress.text.trim() );
+    if ( arg == "ssl" ) { 
+      smtp.setEncryption( "SSL" );
+    } else if ( arg == "tls" ) {
+      smtp.setEncryption( "TLS" );
+    } else {
+      smtp.setEncryption( "None" );
+    }
+    smtp.setUsername( page.widget().userName.text );
+    smtp.setPassword( SetupManager.password() );
+    identity.setTransport( smtp );
+    SetupManager.execute();
+  }
+}
+
+try {
+  ServerTest.testFail.connect( testResultFail );
+  ServerTest.testResult.connect( testOk );
+  page.widget().incommingAddress.textChanged.connect( validateInput );
+  page.pageLeftNext.connect( setup );
+} catch ( e ) {
+  print( e );
+}
+
+validateInput();
diff --git a/resources/imap/wizard/imapwizard.ui b/resources/imap/wizard/imapwizard.ui
new file mode 100644 (file)
index 0000000..d45d31b
--- /dev/null
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>imapWizard</class>
+ <widget class="QWidget" name="imapWizard">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>300</height>
+   </rect>
+  </property>
+  <layout class="QFormLayout" name="formLayout">
+   <property name="fieldGrowthPolicy">
+    <enum>QFormLayout::ExpandingFieldsGrow</enum>
+   </property>
+   <item row="0" column="0">
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>Username:</string>
+     </property>
+     <property name="buddy">
+      <cstring>userName</cstring>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="1">
+    <widget class="KLineEdit" name="userName"/>
+   </item>
+   <item row="1" column="0">
+    <widget class="QLabel" name="label_2">
+     <property name="text">
+      <string>&amp;Incoming server:</string>
+     </property>
+     <property name="buddy">
+      <cstring>incommingAddress</cstring>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="1">
+    <widget class="KLineEdit" name="incommingAddress"/>
+   </item>
+   <item row="2" column="0">
+    <widget class="QLabel" name="label_4">
+     <property name="text">
+      <string>&amp;Outgoing server:</string>
+     </property>
+     <property name="buddy">
+      <cstring>outgoingAddress</cstring>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="1">
+    <widget class="KLineEdit" name="outgoingAddress">
+     <property name="text">
+      <string/>
+     </property>
+    </widget>
+   </item>
+   <item row="3" column="0" colspan="2">
+    <widget class="QCheckBox" name="disconnectedMode">
+     <property name="text">
+      <string>&amp;Download all messages for offline use</string>
+     </property>
+    </widget>
+   </item>
+   <item row="4" column="0" colspan="2">
+    <spacer name="verticalSpacer_2">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>40</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KLineEdit</class>
+   <extends>QLineEdit</extends>
+   <header>klineedit.h</header>
+  </customwidget>
+ </customwidgets>
+ <tabstops>
+  <tabstop>userName</tabstop>
+  <tabstop>incommingAddress</tabstop>
+  <tabstop>outgoingAddress</tabstop>
+  <tabstop>disconnectedMode</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/resources/kalarm/CMakeLists.txt b/resources/kalarm/CMakeLists.txt
new file mode 100644 (file)
index 0000000..2529086
--- /dev/null
@@ -0,0 +1,3 @@
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_kalarm_resource\")
+add_subdirectory(kalarm)
+add_subdirectory(kalarmdir)
diff --git a/resources/kalarm/Messages.sh b/resources/kalarm/Messages.sh
new file mode 100755 (executable)
index 0000000..6d8398f
--- /dev/null
@@ -0,0 +1,4 @@
+#! /usr/bin/env bash
+$EXTRACTRC `find . -name \*.kcfg -o -name \*.ui` >> rc.cpp || exit 11
+$XGETTEXT `find . \( ! -path "./kdepim-runtime/*" \) -a \( -name "*.cpp" -o -name "*.h" \)` -o $podir/akonadi_kalarm_resource.pot
+rm -f rc.cpp
diff --git a/resources/kalarm/kalarm/CMakeLists.txt b/resources/kalarm/kalarm/CMakeLists.txt
new file mode 100644 (file)
index 0000000..93de1d9
--- /dev/null
@@ -0,0 +1,49 @@
+include_directories(
+    ${CMAKE_CURRENT_BINARY_DIR}
+    ${CMAKE_CURRENT_SOURCE_DIR}/../shared
+    ${CMAKE_CURRENT_SOURCE_DIR}/../../ical/shared
+)
+
+
+
+########### next target ###############
+add_definitions(-DSETTINGS_NAMESPACE=Akonadi_KAlarm_Resource)
+
+set(kalarmresource_SRCS
+    ${CMAKE_CURRENT_SOURCE_DIR}/../../ical/shared/icalresourcebase.cpp
+    kalarmresource.cpp
+    ../shared/kalarmresourcecommon.cpp
+    ../shared/alarmtyperadiowidget.cpp
+)
+
+install(FILES kalarmresource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents")
+
+ki18n_wrap_ui(kalarmresource_SRCS ../shared/alarmtyperadiowidget.ui)
+kconfig_add_kcfg_files(kalarmresource_SRCS settings.kcfgc)
+kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/kalarmresource.kcfg org.kde.Akonadi.KAlarm.Settings)
+qt5_add_dbus_adaptor(kalarmresource_SRCS
+    ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.KAlarm.Settings.xml settings.h Akonadi_KAlarm_Resource::Settings icalsettingsadaptor ICalSettingsAdaptor)
+
+ecm_qt_declare_logging_category(kalarmresource_SRCS HEADER kalarmresource_debug.h IDENTIFIER KALARMRESOURCE_LOG CATEGORY_NAME log_kalarmresource)
+
+add_custom_target(kalarm_resource_xml ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.KAlarm.Settings.xml)
+
+add_executable(akonadi_kalarm_resource ${kalarmresource_SRCS})
+
+if( APPLE )
+    set_target_properties(akonadi_kalarm_resource PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../Info.plist.template)
+    set_target_properties(akonadi_kalarm_resource PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.KAlarm")
+    set_target_properties(akonadi_kalarm_resource PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi KAlarm Resource")
+endif ()
+
+target_link_libraries(akonadi_kalarm_resource
+                      KF5::AlarmCalendar
+                      KF5::AkonadiCore
+                      KF5::CalendarCore
+                      KF5::KIOCore
+                      KF5::AkonadiAgentBase
+                      KF5::DBusAddons
+                      akonadi-singlefileresource
+                     )
+
+install(TARGETS akonadi_kalarm_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
diff --git a/resources/kalarm/kalarm/kalarmresource.cpp b/resources/kalarm/kalarm/kalarmresource.cpp
new file mode 100644 (file)
index 0000000..d901e86
--- /dev/null
@@ -0,0 +1,513 @@
+/*
+ *  kalarmresource.cpp  -  Akonadi resource for KAlarm
+ *  Program:  kalarm
+ *  Copyright © 2009-2014 by David Jarvie <djarvie@kde.org>
+ *
+ *  This library is free software; you can redistribute it and/or modify it
+ *  under the terms of the GNU Library General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or (at your
+ *  option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful, but WITHOUT
+ *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ *  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ *  License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public License
+ *  along with this library; see the file COPYING.LIB.  If not, write to the
+ *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ *  02110-1301, USA.
+ */
+
+#include "kalarmresource.h"
+#include "kalarmresourcecommon.h"
+#include "alarmtyperadiowidget.h"
+
+#include <kalarmcal/compatibilityattribute.h>
+#include <kalarmcal/kacalendar.h>
+#include <kalarmcal/kaevent.h>
+
+#include <attributefactory.h>
+#include <collectionfetchjob.h>
+#include <collectionfetchscope.h>
+#include <collectionmodifyjob.h>
+
+#include <KCalCore/MemoryCalendar>
+#include <KCalCore/Incidence>
+
+#include <KLocalizedString>
+#include "kalarmresource_debug.h"
+
+using namespace Akonadi;
+using namespace Akonadi_KAlarm_Resource;
+using namespace KAlarmCal;
+using KAlarmResourceCommon::errorMessage;
+
+KAlarmResource::KAlarmResource(const QString &id)
+    : ICalResourceBase(id),
+      mCompatibility(KACalendar::Incompatible),
+      mFileCompatibility(KACalendar::Incompatible),
+      mVersion(KACalendar::MixedFormat),
+      mFileVersion(KACalendar::IncompatibleFormat),
+      mHaveReadFile(false),
+      mFetchedAttributes(false)
+{
+    qCDebug(KALARMRESOURCE_LOG) << id;
+    KAlarmResourceCommon::initialise(this);
+    initialise(mSettings->alarmTypes(), QStringLiteral("kalarm"));
+    connect(mSettings, &Settings::configChanged, this, &KAlarmResource::settingsChanged);
+
+    // Start a job to fetch the collection attributes
+    fetchCollection(SLOT(collectionFetchResult(KJob*)));
+}
+
+KAlarmResource::~KAlarmResource()
+{
+}
+
+/******************************************************************************
+* Customize the configuration dialog before it is displayed.
+*/
+void KAlarmResource::customizeConfigDialog(SingleFileResourceConfigDialog<Settings> *dlg)
+{
+    ICalResourceBase::customizeConfigDialog(dlg);
+    mTypeSelector = new AlarmTypeRadioWidget(dlg);
+    const QStringList types = mSettings->alarmTypes();
+    CalEvent::Type alarmType = CalEvent::ACTIVE;
+    if (!types.isEmpty()) {
+        alarmType = CalEvent::type(types[0]);
+    }
+    mTypeSelector->setAlarmType(alarmType);
+    dlg->appendWidget(mTypeSelector);
+    dlg->setMonitorEnabled(false);
+    QString title;
+    switch (alarmType) {
+    case CalEvent::ACTIVE:
+        title = i18nc("@title:window", "Select Active Alarm Calendar");
+        break;
+    case CalEvent::ARCHIVED:
+        title = i18nc("@title:window", "Select Archived Alarm Calendar");
+        break;
+    case CalEvent::TEMPLATE:
+        title = i18nc("@title:window", "Select Alarm Template Calendar");
+        break;
+    default:
+        return;
+    }
+    dlg->setWindowTitle(title);
+}
+
+/******************************************************************************
+* Save extra settings after the configuration dialog has been accepted.
+*/
+void KAlarmResource::configDialogAcceptedActions(SingleFileResourceConfigDialog<Settings> *)
+{
+    mSettings->setAlarmTypes(CalEvent::mimeTypes(mTypeSelector->alarmType()));
+    mSettings->save();
+}
+
+/******************************************************************************
+* Reimplemented to fetch collection attributes after creating the collection.
+*/
+void KAlarmResource::retrieveCollections()
+{
+    qCDebug(KALARMRESOURCE_LOG);
+    mSupportedMimetypes = mSettings->alarmTypes();
+    ICalResourceBase::retrieveCollections();
+    fetchCollection(SLOT(collectionFetchResult(KJob*)));
+}
+
+/******************************************************************************
+* Called when the collection fetch job completes.
+* Check the calendar file's compatibility status if pending.
+*/
+void KAlarmResource::collectionFetchResult(KJob *j)
+{
+    if (j->error()) {
+        // An error occurred. Note that if it's a new resource, it will complain
+        // about an invalid collection if the collection has not yet been created.
+        qCDebug(KALARMRESOURCE_LOG) << "Error: " << j->errorString();
+    } else {
+        bool firstTime = !mFetchedAttributes;
+        mFetchedAttributes = true;
+        CollectionFetchJob *job = static_cast<CollectionFetchJob *>(j);
+        const Collection::List collections = job->collections();
+        if (collections.isEmpty()) {
+            qCDebug(KALARMRESOURCE_LOG) << "Error: resource's collection not found";
+        } else {
+            // Check whether calendar file format needs to be updated
+            qCDebug(KALARMRESOURCE_LOG) << "Fetched collection";
+            const Collection &c(collections[0]);
+            if (firstTime  &&  mSettings->path().isEmpty()) {
+                // Initialising a resource which seems to have no stored
+                // settings config file. Recreate the settings.
+                static const Collection::Rights writableRights = Collection::CanChangeItem | Collection::CanCreateItem | Collection::CanDeleteItem;
+                qCDebug(KALARMRESOURCE_LOG) << "Recreating config for remote id:" << c.remoteId();
+                mSettings->setPath(c.remoteId());
+                mSettings->setDisplayName(c.name());
+                mSettings->setAlarmTypes(c.contentMimeTypes());
+                mSettings->setReadOnly((c.rights() & writableRights) != writableRights);
+                mSettings->save();
+                synchronize();   // tell the server to use the new config
+            }
+            checkFileCompatibility(c, true);
+        }
+    }
+}
+
+/******************************************************************************
+* Reimplemented to read data from the given file.
+* This is called every time the resource starts up (see SingleFileResourceBase
+* constructor).
+* Find the calendar file's compatibility with the current KAlarm format.
+* The file is always local; loading from the network is done automatically if
+* needed.
+*/
+bool KAlarmResource::readFromFile(const QString &fileName)
+{
+    qCDebug(KALARMRESOURCE_LOG) << fileName;
+//TODO Notify user if error occurs on next line
+    if (!ICalResourceBase::readFromFile(fileName)) {
+        return false;
+    }
+    if (calendar()->incidences().isEmpty()) {
+        // It's a new file. Set up the KAlarm custom property.
+        KACalendar::setKAlarmVersion(calendar());
+    }
+    mFileCompatibility = KAlarmResourceCommon::getCompatibility(fileStorage(), mFileVersion);
+    mHaveReadFile = true;
+
+    if (mFetchedAttributes) {
+        // The old calendar file version and compatibility have been read from
+        // the database. Check if the file format needs to be converted.
+        checkFileCompatibility();
+    }
+    return true;
+}
+
+/******************************************************************************
+* To be called when the collection attributes have been fetched, or if they
+* have changed.
+* Check if the recorded calendar version and compatibility are different from
+* the actual backend file, and if necessary convert the calendar in memory to
+* the current version.
+* If 'createAttribute' is true, the CompatibilityAttribute will be created if
+* it does not already exist.
+*/
+void KAlarmResource::checkFileCompatibility(const Collection &collection, bool createAttribute)
+{
+    if (collection.isValid()
+            &&  collection.hasAttribute<CompatibilityAttribute>()) {
+        // Update our note of the calendar version and compatibility
+        const CompatibilityAttribute *attr = collection.attribute<CompatibilityAttribute>();
+        mCompatibility = attr->compatibility();
+        mVersion       = attr->version();
+        createAttribute = false;
+    }
+    if (mHaveReadFile
+            && (createAttribute
+                ||  mFileCompatibility != mCompatibility  ||  mFileVersion != mVersion)) {
+        // The actual file's version and compatibility are different from
+        // those in the Akonadi database, so update the database attributes.
+        mCompatibility = mFileCompatibility;
+        mVersion       = mFileVersion;
+        const Collection c(collection);
+        if (c.isValid()) {
+            KAlarmResourceCommon::setCollectionCompatibility(c, mCompatibility, mVersion);
+        } else {
+            fetchCollection(SLOT(setCompatibility(KJob*)));
+        }
+    }
+}
+
+/******************************************************************************
+* Called when a collection fetch job completes.
+* Set the compatibility attribute for the collection.
+*/
+void KAlarmResource::setCompatibility(KJob *j)
+{
+    CollectionFetchJob *job = static_cast<CollectionFetchJob *>(j);
+    if (j->error()) {
+        qCDebug(KALARMRESOURCE_LOG) << "Error: " << j->errorString();
+    } else if (job->collections().isEmpty()) {
+        qCDebug(KALARMRESOURCE_LOG) << "Error: resource's collection not found";
+    } else {
+        KAlarmResourceCommon::setCollectionCompatibility(job->collections().at(0), mCompatibility, mVersion);
+    }
+}
+
+/******************************************************************************
+* Reimplemented to write data to the given file.
+* The file is always local.
+*/
+bool KAlarmResource::writeToFile(const QString &fileName)
+{
+    qCDebug(KALARMRESOURCE_LOG) << fileName;
+    if (calendar() && calendar()->incidences().isEmpty()) {
+        // It's an empty file. Set up the KAlarm custom property.
+        KACalendar::setKAlarmVersion(calendar());
+    }
+    return ICalResourceBase::writeToFile(fileName);
+}
+
+/******************************************************************************
+* Retrieve an event from the calendar, whose uid and Akonadi id are given by
+* 'item' (item.remoteId() and item.id() respectively).
+* Set the event into a new item's payload, and signal its retrieval by calling
+* itemRetrieved(newitem).
+*/
+bool KAlarmResource::doRetrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts)
+{
+    Q_UNUSED(parts);
+    const QString rid = item.remoteId();
+    const KCalCore::Event::Ptr kcalEvent = calendar()->event(rid);
+    if (!kcalEvent) {
+        qCWarning(KALARMRESOURCE_LOG) << "Event not found:" << rid;
+        Q_EMIT error(errorMessage(KAlarmResourceCommon::UidNotFound, rid));
+        return false;
+    }
+
+    if (kcalEvent->alarms().isEmpty()) {
+        qCWarning(KALARMRESOURCE_LOG) << "KCalCore::Event has no alarms:" << rid;
+        Q_EMIT error(errorMessage(KAlarmResourceCommon::EventNoAlarms, rid));
+        return false;
+    }
+
+    KAEvent event(kcalEvent);
+    const QString mime = CalEvent::mimeType(event.category());
+    if (mime.isEmpty()) {
+        qCWarning(KALARMRESOURCE_LOG) << "KAEvent has no alarms:" << rid;
+        Q_EMIT error(errorMessage(KAlarmResourceCommon::EventNoAlarms, rid));
+        return false;
+    }
+    event.setCompatibility(mCompatibility);
+    const Item newItem = KAlarmResourceCommon::retrieveItem(item, event);
+    itemRetrieved(newItem);
+    return true;
+}
+
+/******************************************************************************
+* Called when the resource settings have changed.
+* Update the supported mime types if the AlarmTypes setting has changed.
+* Update the storage format if UpdateStorageFormat setting = true.
+*/
+void KAlarmResource::settingsChanged()
+{
+    qCDebug(KALARMRESOURCE_LOG);
+    const QStringList mimeTypes = mSettings->alarmTypes();
+    if (mimeTypes != mSupportedMimetypes) {
+        mSupportedMimetypes = mimeTypes;
+    }
+
+    if (mSettings->updateStorageFormat()) {
+        // This is a flag to request that the backend calendar storage format should
+        // be updated to the current KAlarm format.
+        qCDebug(KALARMRESOURCE_LOG) << "Update storage format";
+        fetchCollection(SLOT(updateFormat(KJob*)));
+    }
+}
+
+/******************************************************************************
+* Called when a collection fetch job completes.
+* Update the backend calendar storage format to the current KAlarm format.
+*/
+void KAlarmResource::updateFormat(KJob *j)
+{
+    CollectionFetchJob *job = static_cast<CollectionFetchJob *>(j);
+    if (j->error()) {
+        qCDebug(KALARMRESOURCE_LOG) << "Error: " << j->errorString();
+    } else if (job->collections().isEmpty()) {
+        qCDebug(KALARMRESOURCE_LOG) << "Error: resource's collection not found";
+    } else {
+        const Collection c(job->collections().at(0));
+        if (c.hasAttribute<CompatibilityAttribute>()) {
+            const CompatibilityAttribute *attr = c.attribute<CompatibilityAttribute>();
+            if (attr->compatibility() != mCompatibility) {
+                qCDebug(KALARMRESOURCE_LOG) << "Compatibility changed:" << mCompatibility << "->" << attr->compatibility();
+            }
+        }
+        switch (mCompatibility) {
+        case KACalendar::Current:
+            qCWarning(KALARMRESOURCE_LOG) << "Already current storage format";
+            break;
+        case KACalendar::Incompatible:
+        default:
+            qCWarning(KALARMRESOURCE_LOG) << "Incompatible storage format: compat=" << mCompatibility;
+            break;
+        case KACalendar::Converted:
+        case KACalendar::Convertible: {
+            if (mSettings->readOnly()) {
+                qCWarning(KALARMRESOURCE_LOG) << "Cannot update storage format for a read-only resource";
+                break;
+            }
+            // Update the backend storage format to the current KAlarm format
+            const QString filename = fileStorage()->fileName();
+            qCDebug(KALARMRESOURCE_LOG) << "Updating storage for" << filename;
+            KACalendar::setKAlarmVersion(fileStorage()->calendar());
+            if (!writeToFile(filename)) {
+                qCWarning(KALARMRESOURCE_LOG) << "Error updating calendar storage format";
+                break;
+            }
+            // Prevent a new file read being triggered by writeToFile(), which
+            // would replace the current Collection by a new one.
+            mCurrentHash = calculateHash(filename);
+
+            mCompatibility = mFileCompatibility = KACalendar::Current;
+            mVersion = mFileVersion = KACalendar::CurrentFormat;
+            KAlarmResourceCommon::setCollectionCompatibility(c, mCompatibility, 0);
+            break;
+        }
+        }
+        mSettings->setUpdateStorageFormat(false);
+        mSettings->save();
+    }
+}
+
+/******************************************************************************
+* Called when an item has been added to the collection.
+* Store the event in the calendar, and set its Akonadi remote ID to the
+* KAEvent's UID.
+*/
+void KAlarmResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &)
+{
+    if (!checkItemAddedChanged<KAEvent>(item, CheckForAdded)) {
+        return;
+    }
+    if (mCompatibility != KACalendar::Current) {
+        qCWarning(KALARMRESOURCE_LOG) << "Calendar not in current format";
+        cancelTask(errorMessage(KAlarmResourceCommon::NotCurrentFormat));
+        return;
+    }
+    const KAEvent event = item.payload<KAEvent>();
+    KCalCore::Event::Ptr kcalEvent(new KCalCore::Event);
+    event.updateKCalEvent(kcalEvent, KAEvent::UID_SET);
+    if (!calendar()->addIncidence(kcalEvent)) {
+        qCritical() << "Error adding event with id" << event.id() << ", item id" << item.id();
+        cancelTask(errorMessage(KAlarmResourceCommon::CalendarAdd, event.id()));
+        return;
+    }
+
+    Item it(item);
+    it.setRemoteId(kcalEvent->uid());
+    scheduleWrite();
+    changeCommitted(it);
+}
+
+/******************************************************************************
+* Called when an item has been changed.
+* Store the changed event in the calendar, and delete the original event.
+*/
+void KAlarmResource::itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &parts)
+{
+    Q_UNUSED(parts)
+    if (!checkItemAddedChanged<KAEvent>(item, CheckForChanged)) {
+        return;
+    }
+    QString errorMsg;
+    if (mCompatibility != KACalendar::Current) {
+        qCWarning(KALARMRESOURCE_LOG) << "Calendar not in current format";
+        cancelTask(errorMessage(KAlarmResourceCommon::NotCurrentFormat));
+        return;
+    }
+    const KAEvent event = KAlarmResourceCommon::checkItemChanged(item, errorMsg);
+    if (!event.isValid()) {
+        if (errorMsg.isEmpty()) {
+            changeProcessed();
+        } else {
+            cancelTask(errorMsg);
+        }
+        return;
+    }
+
+    KCalCore::Incidence::Ptr incidence = calendar()->incidence(item.remoteId());
+    if (incidence) {
+        if (incidence->isReadOnly()) {
+            qCWarning(KALARMRESOURCE_LOG) << "Event is read only:" << event.id();
+            cancelTask(errorMessage(KAlarmResourceCommon::EventReadOnly, event.id()));
+            return;
+        }
+        if (incidence->type() == KCalCore::Incidence::TypeEvent) {
+            calendar()->deleteIncidence(incidence);   // it's not an Event
+            incidence.clear();
+        } else {
+            KCalCore::Event::Ptr ev(incidence.staticCast<KCalCore::Event>());
+            event.updateKCalEvent(ev, KAEvent::UID_SET);
+            calendar()->setModified(true);
+        }
+    }
+    if (!incidence) {
+        // not in the calendar yet, should not happen -> add it
+        KCalCore::Event::Ptr kcalEvent(new KCalCore::Event);
+        event.updateKCalEvent(kcalEvent, KAEvent::UID_SET);
+        calendar()->addIncidence(kcalEvent);
+    }
+    scheduleWrite();
+    changeCommitted(item);
+}
+
+/******************************************************************************
+* Called when a collection has been changed.
+* Determine the calendar file's storage format.
+*/
+void KAlarmResource::collectionChanged(const Akonadi::Collection &collection)
+{
+    ICalResourceBase::collectionChanged(collection);
+
+    mFetchedAttributes = true;
+    // Check whether calendar file format needs to be updated
+    checkFileCompatibility(collection);
+}
+
+/******************************************************************************
+* Retrieve all events from the calendar, and set each into a new item's
+* payload. Items are identified by their remote IDs. The Akonadi ID is not
+* used.
+* Signal the retrieval of the items by calling itemsRetrieved(items), which
+* updates Akonadi with any changes to the items. itemsRetrieved() compares
+* the new and old items, matching them on the remoteId(). If the flags or
+* payload have changed, or the Item has any new Attributes, the Akonadi
+* storage is updated.
+*/
+void KAlarmResource::doRetrieveItems(const Akonadi::Collection &collection)
+{
+    qCDebug(KALARMRESOURCE_LOG);
+
+    // Set the collection's compatibility status
+    KAlarmResourceCommon::setCollectionCompatibility(collection, mCompatibility, mVersion);
+
+    // Retrieve events from the calendar
+    const KCalCore::Event::List events = calendar()->events();
+    Item::List items;
+    foreach (const KCalCore::Event::Ptr &kcalEvent, events) {
+        if (kcalEvent->alarms().isEmpty()) {
+            qCWarning(KALARMRESOURCE_LOG) << "KCalCore::Event has no alarms:" << kcalEvent->uid();
+            continue;    // ignore events without alarms
+        }
+
+        const KAEvent event(kcalEvent);
+        const QString mime = CalEvent::mimeType(event.category());
+        if (mime.isEmpty()) {
+            qCWarning(KALARMRESOURCE_LOG) << "KAEvent has no alarms:" << event.id();
+            continue;   // event has no usable alarms
+        }
+
+        Item item(mime);
+        item.setRemoteId(kcalEvent->uid());
+        item.setPayload(event);
+        items << item;
+    }
+    itemsRetrieved(items);
+}
+
+/******************************************************************************
+* Execute a CollectionFetchJob to fetch details of this resource's collection.
+*/
+CollectionFetchJob *KAlarmResource::fetchCollection(const char *slot)
+{
+    CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel);
+    job->fetchScope().setResource(identifier());
+    connect(job, SIGNAL(result(KJob*)), slot);
+    return job;
+}
+
+AKONADI_RESOURCE_MAIN(KAlarmResource)
diff --git a/resources/kalarm/kalarm/kalarmresource.desktop b/resources/kalarm/kalarm/kalarmresource.desktop
new file mode 100644 (file)
index 0000000..139bcea
--- /dev/null
@@ -0,0 +1,98 @@
+[Desktop Entry]
+Name=KAlarm Calendar File
+Name[bg]=Календарен файл на KAlarm
+Name[bs]=KAlarm Kalendar Datoteka
+Name[ca]=Fitxer de calendari del KAlarm
+Name[ca@valencia]=Fitxer de calendari del KAlarm
+Name[cs]=Soubor kalendáře KAlarm
+Name[da]=KAlarm-kalenderfil
+Name[de]=KAlarm-Kalenderdatei
+Name[el]=Αρχείο ημερολογίου KAlarm
+Name[en_GB]=KAlarm Calendar File
+Name[es]=Archivo de calendario de KAlarm
+Name[et]=KAlarmi kalendrifail
+Name[fi]=KAlarm-kalenteritiedosto
+Name[fr]=Fichier d'agenda KAlarm
+Name[ga]=Comhad Féilire KAlarm
+Name[gl]=Ficheiro de calendario de KAlarm
+Name[hu]=KAlarm naptárfájl
+Name[ia]=File de calendario de KAlarm
+Name[it]=Calendario di KAlarm
+Name[ja]=KAlarm カレンダーファイル
+Name[kk]=KAlarm күнтізбе файлы
+Name[km]=ឯកសារ​ប្រតិទិន​របស់ KAlarm
+Name[ko]=KAlarm 달력 파일
+Name[lt]=KAlarm kalendoriaus failas
+Name[lv]=KAlarm kalendāra fails
+Name[nb]=KAlarm kalenderfil
+Name[nds]=KAlarm-Kalennerdatei
+Name[nl]=KAlarm-agendabestand
+Name[nn]=KAlarm-kalenderfil
+Name[pa]=ਕੇ-ਅਲਾਰਮ ਕੈਲੰਡਰ ਫਾਇਲ
+Name[pl]=Plik kalendarza KAlarm
+Name[pt]=Ficheiro de Calendário do KAlarm
+Name[pt_BR]=Arquivo de calendário do KAlarm
+Name[ru]=Файл календаря KAlarm
+Name[sk]=Súbor kalendára KAlarm
+Name[sl]=Koledarska datoteka KAlarma
+Name[sr]=К‑алармов календарски фајл
+Name[sr@ijekavian]=К‑алармов календарски фајл
+Name[sr@ijekavianlatin]=K‑alarmov kalendarski fajl
+Name[sr@latin]=K‑alarmov kalendarski fajl
+Name[sv]=Kalarm-kalenderfil
+Name[tr]=KAlarm Takvim Dosyası
+Name[uk]=Файл календаря KAlarm
+Name[x-test]=xxKAlarm Calendar Filexx
+Name[zh_CN]=KAlarm 日历文件
+Name[zh_TW]=KAlarm 行事曆檔案
+Comment=Loads data from a KAlarm calendar file
+Comment[bg]=Зареждане на данни от календарен файл KAlarm
+Comment[bs]=Učitava podatke iz KAlarm kalendar datoteke
+Comment[ca]=Carrega les dades des d'un fitxer de calendari del KAlarm
+Comment[ca@valencia]=Carrega les dades des d'un fitxer de calendari del KAlarm
+Comment[cs]=Načítá data ze souboru kalendáře KAlarm
+Comment[da]=Indlæser data fra KAlarm-kalenderfil
+Comment[de]=Lädt Daten aus einer KAlarm-Kalenderdatei
+Comment[el]=Φόρτωση δεδομένων από ένα αρχείο ημερολογίου του KAlarm 
+Comment[en_GB]=Loads data from a KAlarm calendar file
+Comment[es]=Carga datos de un archivo de calendario de KAlarm
+Comment[et]=Andmete laadimine KAlarmi kalendrifailist
+Comment[fi]=Noutaa tietoa KAlarmin kalenteritiedostosta
+Comment[fr]=Charge les données depuis un fichier d'agenda KAlarm
+Comment[ga]=Breiseán a luchtaíonn sonraí ó chomhad féilire KAlarm
+Comment[gl]=Carga datos desde un ficheiro de calendario do KAlarm.
+Comment[hu]=Adatok betöltése egy KAlarm naptárfájlból
+Comment[ia]=Carga datos ex un file de calendario de KAlarm
+Comment[it]=Carica dati da un calendario di KAlarm
+Comment[ja]=KAlarm のカレンダーファイルからデータを読み込みます
+Comment[kk]=KAlarm-дың күнтізбе файлдан деректі алу
+Comment[km]=ផ្ទុក​ទិន្នន័យ​ពី​ឯកសារ​ប្រតិទិន​របស់ KAlarm
+Comment[ko]=KAlarm 달력 파일에서 데이터를 가져옵니다
+Comment[lt]=Įkelia duomenis iš KAlarm kalendoriaus failo
+Comment[lv]=Ielādē datus no KAlarm kalendāra faila
+Comment[nb]=Laster data fra en KAlarm kalenderfil
+Comment[nds]=Laadt Daten ut en KAlarm-Kalennerdatei
+Comment[nl]=Laadt gegevens van een KAlarm-agendabestand
+Comment[nn]=Lastar data frå ei KAlarm-kalenderfil
+Comment[pl]=Wczytuje dane z pliku kalendarza KAlarm
+Comment[pt]=Carrega os dados de um ficheiro de calendário do KAlarm
+Comment[pt_BR]=Carrega os dados de um arquivo de calendário do KAlarm
+Comment[ru]=Загрузка данных из файла календаря KAlarm
+Comment[sk]=Načíta dáta zo súboru kalendára KAlarm
+Comment[sl]=Naloži podatke iz koledarske datoteke KAlarma
+Comment[sr]=Учитава податке из К‑алармовог календарског фајла
+Comment[sr@ijekavian]=Учитава податке из К‑алармовог календарског фајла
+Comment[sr@ijekavianlatin]=Učitava podatke iz K‑alarmovog kalendarskog fajla
+Comment[sr@latin]=Učitava podatke iz K‑alarmovog kalendarskog fajla
+Comment[sv]=Laddar data från en Kalarm-kalenderfil
+Comment[tr]=KAlarm takvim dosyasından veri yükler
+Comment[uk]=Завантажує дані з файла календаря KAlarm
+Comment[x-test]=xxLoads data from a KAlarm calendar filexx
+Comment[zh_CN]=从 KAlarm 日历文件载入数据
+Comment[zh_TW]=從 KAlarm 行事曆檔載入資料
+Type=AkonadiResource
+Exec=akonadi_kalarm_resource
+Icon=kalarm
+X-Akonadi-MimeTypes=application/x-vnd.kde.alarm,application/x-vnd.kde.alarm.active,application/x-vnd.kde.alarm.archived,application/x-vnd.kde.alarm.template
+X-Akonadi-Capabilities=Resource
+X-Akonadi-Identifier=akonadi_kalarm_resource
diff --git a/resources/kalarm/kalarm/kalarmresource.h b/resources/kalarm/kalarm/kalarmresource.h
new file mode 100644 (file)
index 0000000..97918c2
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ *  kalarmresource.h  -  Akonadi resource for KAlarm
+ *  Program:  kalarm
+ *  Copyright © 2009-2014 by David Jarvie <djarvie@kde.org>
+ *
+ *  This library is free software; you can redistribute it and/or modify it
+ *  under the terms of the GNU Library General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or (at your
+ *  option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful, but WITHOUT
+ *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ *  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ *  License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public License
+ *  along with this library; see the file COPYING.LIB.  If not, write to the
+ *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ *  02110-1301, USA.
+ */
+
+#ifndef KALARMRESOURCE_H
+#define KALARMRESOURCE_H
+
+#include "icalresourcebase.h"
+
+#include <kalarmcal/kacalendar.h>
+
+using namespace KAlarmCal;
+
+class KJob;
+namespace Akonadi
+{
+class CollectionFetchJob;
+}
+class AlarmTypeRadioWidget;
+
+class KAlarmResource : public ICalResourceBase
+{
+    Q_OBJECT
+public:
+    explicit KAlarmResource(const QString &id);
+    virtual ~KAlarmResource();
+
+protected:
+    /**
+     * Customize the configuration dialog before it is displayed.
+     */
+    void customizeConfigDialog(Akonadi::SingleFileResourceConfigDialog<Akonadi_KAlarm_Resource::Settings> *) Q_DECL_OVERRIDE;
+    void configDialogAcceptedActions(Akonadi::SingleFileResourceConfigDialog<Akonadi_KAlarm_Resource::Settings> *) Q_DECL_OVERRIDE;
+
+    void doRetrieveItems(const Akonadi::Collection &) Q_DECL_OVERRIDE;
+    bool doRetrieveItem(const Akonadi::Item &, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+    bool readFromFile(const QString &fileName) Q_DECL_OVERRIDE;
+    bool writeToFile(const QString &fileName) Q_DECL_OVERRIDE;
+    void itemAdded(const Akonadi::Item &, const Akonadi::Collection &) Q_DECL_OVERRIDE;
+    void itemChanged(const Akonadi::Item &, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+    void collectionChanged(const Akonadi::Collection &) Q_DECL_OVERRIDE;
+    void retrieveCollections() Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void settingsChanged();
+    void collectionFetchResult(KJob *);
+    void updateFormat(KJob *);
+    void setCompatibility(KJob *);
+
+private:
+    void checkFileCompatibility(const Akonadi::Collection & = Akonadi::Collection(), bool createAttribute = false);
+    Akonadi::CollectionFetchJob *fetchCollection(const char *slot);
+
+    AlarmTypeRadioWidget *mTypeSelector;
+    KACalendar::Compat    mCompatibility;
+    KACalendar::Compat    mFileCompatibility;  // calendar file compatibility found by readFromFile()
+    int                   mVersion;            // calendar format version
+    int                   mFileVersion;        // calendar format version found by readFromFile()
+    bool                  mHaveReadFile;       // the calendar file has been read
+    bool                  mFetchedAttributes;  // attributes have been fetched after initialisation
+};
+
+#endif
+
diff --git a/resources/kalarm/kalarm/kalarmresource.kcfg b/resources/kalarm/kalarm/kalarmresource.kcfg
new file mode 100644 (file)
index 0000000..5e096fa
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:kcfg="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
+      http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
+  <kcfgfile arg="true" />
+  <group name="General">
+    <entry name="Path" type="Path">
+      <label>Path to KAlarm calendar file.</label>
+      <default></default>
+    </entry>
+    <entry name="DisplayName" type="String">
+      <label>Display name.</label>
+      <default></default>
+    </entry>
+    <entry name="ReadOnly" type="Bool">
+      <label>Do not change the actual backend data.</label>
+      <default>false</default>
+    </entry>
+    <entry name="MonitorFile" type="Bool">
+      <label>Monitor file for changes.</label>
+      <default>true</default>
+    </entry>
+    <entry name="AlarmTypes" type="StringList">
+      <label>Alarm types.</label>
+    </entry>
+    <entry name="UpdateStorageFormat" type="Bool">
+      <label>Update backend storage format.</label>
+      <default>false</default>
+    </entry>
+  </group>
+</kcfg>
diff --git a/resources/kalarm/kalarm/settings.kcfgc b/resources/kalarm/kalarm/settings.kcfgc
new file mode 100644 (file)
index 0000000..112555d
--- /dev/null
@@ -0,0 +1,10 @@
+File=kalarmresource.kcfg
+ClassName=Settings
+Mutators=true
+ItemAccessors=true
+SetUserTexts=true
+Singleton=false
+#IncludeFiles=
+GlobalEnums=true
+NameSpace=Akonadi_KAlarm_Resource
+
diff --git a/resources/kalarm/kalarmdir/CMakeLists.txt b/resources/kalarm/kalarmdir/CMakeLists.txt
new file mode 100644 (file)
index 0000000..cd6006c
--- /dev/null
@@ -0,0 +1,45 @@
+include_directories(
+    ${CMAKE_CURRENT_BINARY_DIR}
+    ${CMAKE_CURRENT_SOURCE_DIR}/../shared
+)
+
+########### next target ###############
+add_definitions(-DSETTINGS_NAMESPACE=Akonadi_KAlarm_Dir_Resource)
+
+set(kalarmdirresource_SRCS
+    kalarmdirresource_debug.cpp
+    settingsdialog.cpp
+    kalarmdirresource.cpp
+    ../shared/kalarmresourcecommon.cpp
+    ../shared/alarmtypewidget.cpp
+)
+
+install(FILES kalarmdirresource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents")
+
+ki18n_wrap_ui(kalarmdirresource_SRCS settingsdialog.ui ../shared/alarmtypewidget.ui)
+kconfig_add_kcfg_files(kalarmdirresource_SRCS settings.kcfgc)
+kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/kalarmdirresource.kcfg org.kde.Akonadi.KAlarmDir.Settings)
+qt5_add_dbus_adaptor(kalarmdirresource_SRCS
+    ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.KAlarmDir.Settings.xml settings.h Akonadi_KAlarm_Dir_Resource::Settings kalarmdirsettingsadaptor KAlarmDirSettingsAdaptor)
+
+ecm_qt_declare_logging_category(kalarmdirresource_SRCS HEADER kalarmdirresource_debug.h IDENTIFIER KALARMDIRRESOURCE_LOG CATEGORY_NAME log_kalarmdirresource)
+
+add_custom_target(kalarmdir_resource_xml ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.KAlarmDir.Settings.xml)
+
+add_executable(akonadi_kalarm_dir_resource ${kalarmdirresource_SRCS})
+
+if( APPLE )
+    set_target_properties(akonadi_kalarm_dir_resource PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../Info.plist.template)
+    set_target_properties(akonadi_kalarm_dir_resource PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.KAlarmDir")
+    set_target_properties(akonadi_kalarm_dir_resource PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi KAlarm Directory Resource")
+endif ()
+
+target_link_libraries(akonadi_kalarm_dir_resource
+                      KF5::AlarmCalendar
+                      KF5::AkonadiCore
+                      KF5::CalendarCore
+                      KF5::AkonadiAgentBase 
+                      KF5::DBusAddons
+                     )
+
+install(TARGETS akonadi_kalarm_dir_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
diff --git a/resources/kalarm/kalarmdir/autoqpointer.h b/resources/kalarm/kalarmdir/autoqpointer.h
new file mode 100644 (file)
index 0000000..9abf1e7
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ *  autoqpointer.h  -  QPointer which on destruction deletes object
+ *  Program:  kalarm
+ *  Copyright © 2009 by David Jarvie <djarvie@kde.org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License along
+ *  with this program; if not, write to the Free Software Foundation, Inc.,
+ *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef AUTOQPOINTER_H
+#define AUTOQPOINTER_H
+
+#include <QPointer>
+
+/**
+ *  A QPointer which when destructed, deletes the object it points to.
+ *
+ *  @author David Jarvie <djarvie@kde.org>
+ */
+template <class T>
+class AutoQPointer : public QPointer<T>
+{
+public:
+    AutoQPointer() : QPointer<T>() {}
+    AutoQPointer(T *p) : QPointer<T>(p) {}
+    AutoQPointer(const QPointer<T> &p) : QPointer<T>(p) {}
+    ~AutoQPointer()
+    {
+        delete this->data();
+    }
+    AutoQPointer<T> &operator=(const AutoQPointer<T> &p)
+    {
+        QPointer<T>::operator=(p);
+        return *this;
+    }
+    AutoQPointer<T> &operator=(T *p)
+    {
+        QPointer<T>::operator=(p);
+        return *this;
+    }
+};
+
+#endif // AUTOQPOINTER_H
+
diff --git a/resources/kalarm/kalarmdir/kalarmdirresource.cpp b/resources/kalarm/kalarmdir/kalarmdirresource.cpp
new file mode 100644 (file)
index 0000000..08b214d
--- /dev/null
@@ -0,0 +1,1157 @@
+/*
+ *  kalarmdirresource.cpp  -  Akonadi directory resource for KAlarm
+ *  Program:  kalarm
+ *  Copyright © 2011-2014 by David Jarvie <djarvie@kde.org>
+ *  Copyright (c) 2008 Tobias Koenig <tokoe@kde.org>
+ *  Copyright (c) 2008 Bertjan Broeksema <broeksema@kde.org>
+ *
+ *  This library is free software; you can redistribute it and/or modify it
+ *  under the terms of the GNU Library General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or (at your
+ *  option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful, but WITHOUT
+ *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ *  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ *  License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public License
+ *  along with this library; see the file COPYING.LIB.  If not, write to the
+ *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ *  02110-1301, USA.
+ */
+
+#include "kalarmdirresource.h"
+#include "kalarmresourcecommon.h"
+#include "autoqpointer.h"
+#include "kalarmdirsettingsadaptor.h"
+#include "settingsdialog.h"
+
+#include <kalarmcal/kacalendar.h>
+
+#include <KCalCore/FileStorage>
+#include <KCalCore/ICalFormat>
+#include <KCalCore/MemoryCalendar>
+#include <changerecorder.h>
+#include <kdbusconnectionpool.h>
+#include <entitydisplayattribute.h>
+#include <collectionfetchjob.h>
+#include <collectionfetchscope.h>
+#include <collectionmodifyjob.h>
+#include <itemfetchscope.h>
+#include <itemcreatejob.h>
+#include <itemdeletejob.h>
+#include <itemmodifyjob.h>
+
+#include <kdirwatch.h>
+#include <KLocalizedString>
+
+#include <QtCore/QDir>
+#include <QtCore/QDirIterator>
+#include <QtCore/QFile>
+#include <QtCore/QFileInfo>
+#include <QtCore/QTimer>
+#include "kalarmdirresource_debug.h"
+
+using namespace Akonadi;
+using namespace KCalCore;
+using namespace Akonadi_KAlarm_Dir_Resource;
+using KAlarmResourceCommon::errorMessage;
+
+static bool isFileValid(const QString &file);
+
+static const char warningFile[] = "WARNING_README.txt";
+
+#define DEBUG_DATA \
+    qCDebug(KALARMDIRRESOURCE_LOG)<<"ID:Files:"; \
+    foreach (const QString& id, mEvents.uniqueKeys()) { qCDebug(KALARMDIRRESOURCE_LOG)<<id<<":"<<mEvents[id].files; } \
+    qCDebug(KALARMDIRRESOURCE_LOG)<<"File:IDs:"; \
+    foreach (const QString& f, mFileEventIds.uniqueKeys()) { qCDebug(KALARMDIRRESOURCE_LOG)<<f<<":"<<mFileEventIds[f]; }
+
+KAlarmDirResource::KAlarmDirResource(const QString &id)
+    : ResourceBase(id),
+      mSettings(new Settings(config())),
+      mCollectionId(-1),
+      mCompatibility(KACalendar::Incompatible),
+      mCollectionFetched(false),
+      mWaitingToRetrieve(false)
+{
+    qCDebug(KALARMDIRRESOURCE_LOG) << id;
+    KAlarmResourceCommon::initialise(this);
+
+    // Set up the resource
+    new KAlarmDirSettingsAdaptor(mSettings);
+    KDBusConnectionPool::threadConnection().registerObject(QStringLiteral("/Settings"),
+            mSettings, QDBusConnection::ExportAdaptors);
+    connect(mSettings, &Akonadi_KAlarm_Dir_Resource::Settings::configChanged, this, &KAlarmDirResource::settingsChanged);
+
+    changeRecorder()->itemFetchScope().fetchFullPayload();
+    changeRecorder()->fetchCollection(true);
+
+    connect(KDirWatch::self(), &KDirWatch::created, this, &KAlarmDirResource::fileCreated);
+    connect(KDirWatch::self(), &KDirWatch::dirty, this, &KAlarmDirResource::fileChanged);
+    connect(KDirWatch::self(), &KDirWatch::deleted, this, &KAlarmDirResource::fileDeleted);
+
+    // Find the collection which this resource manages
+    CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel);
+    job->fetchScope().setResource(identifier());
+    connect(job, &CollectionFetchJob::result, this, &KAlarmDirResource::collectionFetchResult);
+
+    QTimer::singleShot(0, this, SLOT(loadFiles()));
+}
+
+KAlarmDirResource::~KAlarmDirResource()
+{
+    delete mSettings;
+}
+
+void KAlarmDirResource::aboutToQuit()
+{
+    mSettings->save();
+}
+
+/******************************************************************************
+* Called when the collection fetch job completes.
+* Check the calendar files' compatibility statuses if pending.
+*/
+void KAlarmDirResource::collectionFetchResult(KJob *j)
+{
+    qCDebug(KALARMDIRRESOURCE_LOG);
+    if (j->error()) {
+        qCritical() << "CollectionFetchJob error: " << j->errorString();
+    } else {
+        CollectionFetchJob *job = static_cast<CollectionFetchJob *>(j);
+        Collection::List collections = job->collections();
+        int count = collections.count();
+        qCDebug(KALARMDIRRESOURCE_LOG) << "Count:" << count;
+        if (!count) {
+            qCritical() << "Cannot retrieve this resource's collection";
+        } else {
+            if (count > 1) {
+                qCritical() << "Multiple collections for this resource:" << count;
+            }
+            Collection &c(collections[0]);
+            qCDebug(KALARMDIRRESOURCE_LOG) << "Id:" << c.id() << ", remote id:" << c.remoteId();
+            if (!mCollectionFetched) {
+                bool recreate = mSettings->path().isEmpty();
+                if (!recreate) {
+                    // Remote ID could be path or URL, depending on which version
+                    // of Akonadi created it.
+                    QString rid = c.remoteId();
+                    QUrl url = QUrl::fromLocalFile(mSettings->path());
+                    if (!url.isLocalFile()
+                            || (rid != url.toLocalFile() && rid != url.url() && rid != url.toDisplayString())) {
+                        qCritical() << "Collection remote ID does not match settings: changing settings";
+                        recreate = true;
+                    }
+                }
+                if (recreate) {
+                    // Initialising a resource which seems to have no stored
+                    // settings config file. Recreate the settings.
+                    static const Collection::Rights writableRights = Collection::CanChangeItem | Collection::CanCreateItem | Collection::CanDeleteItem;
+                    qCDebug(KALARMDIRRESOURCE_LOG) << "Recreating config for remote id:" << c.remoteId();
+                    mSettings->setPath(c.remoteId());
+                    mSettings->setDisplayName(c.name());
+                    mSettings->setAlarmTypes(c.contentMimeTypes());
+                    mSettings->setReadOnly((c.rights() & writableRights) != writableRights);
+                    mSettings->save();
+                }
+                mCollectionId = c.id();
+                if (recreate) {
+                    // Load items from the backend files now that their location is known
+                    loadFiles(true);
+                }
+
+                // Set collection's format compatibility flag now that the collection
+                // and its attributes have been fetched.
+                KAlarmResourceCommon::setCollectionCompatibility(c, mCompatibility, mVersion);
+            }
+        }
+    }
+    mCollectionFetched = true;
+    if (mWaitingToRetrieve) {
+        mWaitingToRetrieve = false;
+        retrieveCollections();
+    }
+}
+
+void KAlarmDirResource::configure(WId windowId)
+{
+    qCDebug(KALARMDIRRESOURCE_LOG);
+    // Keep note of the old configuration settings
+    QString     path     = mSettings->path();
+    QString     name     = mSettings->displayName();
+    bool        readOnly = mSettings->readOnly();
+    QStringList types    = mSettings->alarmTypes();
+    // Note: mSettings->monitorFiles() can't change here
+
+    // Use AutoQPointer to guard against crash on application exit while
+    // the dialogue is still open. It prevents double deletion (both on
+    // deletion of parent, and on return from this function).
+    AutoQPointer<SettingsDialog> dlg = new SettingsDialog(windowId, mSettings);
+    if (dlg->exec()) {
+        if (path.isEmpty()) {
+            // Creating a new resource
+            clearCache();   // this deletes any existing collection
+            loadFiles(true);
+            synchronizeCollectionTree();
+        } else if (mSettings->path() != path) {
+            // Directory path change is not allowed for existing resources
+            Q_EMIT configurationDialogRejected();
+            return;
+        } else {
+            bool modify = false;
+            Collection c(mCollectionId);
+            if (mSettings->alarmTypes() != types) {
+                // Settings have changed which might affect the alarm configuration
+                initializeDirectory();   // should only be needed for new resource, but just in case ...
+                CalEvent::Types newTypes = CalEvent::types(mSettings->alarmTypes());
+                CalEvent::Types oldTypes = CalEvent::types(types);
+                changeAlarmTypes(~newTypes & oldTypes);
+                c.setContentMimeTypes(mSettings->alarmTypes());
+                modify = true;
+            }
+            if (mSettings->readOnly() != readOnly
+                    ||  mSettings->displayName() != name) {
+                // Need to change the collection's rights or name
+                c.setRemoteId(directoryName());
+                setNameRights(c);
+                modify = true;
+            }
+            if (modify) {
+                // Update the Akonadi server with the changes
+                CollectionModifyJob *job = new CollectionModifyJob(c);
+                connect(job, &CollectionFetchJob::result, this, &KAlarmDirResource::jobDone);
+            }
+        }
+        Q_EMIT configurationDialogAccepted();
+    } else {
+        Q_EMIT configurationDialogRejected();
+    }
+}
+
+/******************************************************************************
+* Add/remove events to ensure that they match the changed alarm types for the
+* resource.
+*/
+void KAlarmDirResource::changeAlarmTypes(CalEvent::Types removed)
+{
+    DEBUG_DATA;
+    const QString dirPath = directoryName();
+    qCDebug(KALARMDIRRESOURCE_LOG) << dirPath;
+    const QDir dir(dirPath);
+
+    // Read and parse each file in turn
+    QDirIterator it(dir);
+    while (it.hasNext()) {
+        it.next();
+        int removeIfInvalid = 0;
+        QString fileEventId;
+        const QString file = it.fileName();
+        if (!isFileValid(file)) {
+            continue;
+        }
+        QHash<QString, QString>::iterator fit = mFileEventIds.find(file);
+        if (fit != mFileEventIds.end()) {
+            // The file is in the existing file list
+            fileEventId = fit.value();
+            QHash<QString, EventFile>::ConstIterator it = mEvents.constFind(fileEventId);
+            if (it != mEvents.constEnd()) {
+                // And its event is in the existing events list
+                const EventFile &data = it.value();
+                if (data.files[0] == file) {
+                    // It's the file for a used event
+                    if (data.event.category() & removed) {
+                        // The event's type is no longer wanted, so remove it
+                        deleteItem(data.event);
+                        removeEvent(data.event.id(), false);
+                    }
+                    continue;
+                } else {
+                    // The file's event is not currently used - load the
+                    // file and use its event if appropriate.
+                    removeIfInvalid = 0x03;   // remove from mEvents and mFileEventIds
+                }
+            } else {
+                // The file's event isn't in the list of current valid
+                // events - this shouldn't ever happen
+                removeIfInvalid = 0x01;   // remove from mFileEventIds
+            }
+        }
+
+        // Load the file and use its event if appropriate.
+        const QString path = filePath(file);
+        if (QFileInfo(path).isFile()) {
+            if (createItemAndIndex(path, file)) {
+                continue;
+            }
+        }
+        // The event wasn't wanted, so remove from lists
+        if (removeIfInvalid & 0x01) {
+            mFileEventIds.erase(fit);
+        }
+        if (removeIfInvalid & 0x02) {
+            removeEventFile(fileEventId, file);
+        }
+    }
+    DEBUG_DATA;
+    setCompatibility();
+}
+
+/******************************************************************************
+* Called when the resource settings have changed.
+* Update the display name if it has changed.
+* Stop monitoring the directory if 'monitorFiles' is now false.
+* Update the storage format if UpdateStorageFormat setting = true.
+* NOTE: no provision is made for changes to the directory path, since this is
+*       not permitted (would need remote ID changed, plus other complications).
+*/
+void KAlarmDirResource::settingsChanged()
+{
+    qCDebug(KALARMDIRRESOURCE_LOG);
+    const QString display = mSettings->displayName();
+    if (display != name()) {
+        setName(display);
+    }
+
+    const QString dirPath = mSettings->path();
+    if (!dirPath.isEmpty()) {
+        const bool monitoring = KDirWatch::self()->contains(dirPath);
+        if (monitoring  &&  !mSettings->monitorFiles()) {
+            KDirWatch::self()->removeDir(dirPath);
+        } else if (!monitoring  &&  mSettings->monitorFiles()) {
+            KDirWatch::self()->addDir(dirPath, KDirWatch::WatchFiles);
+        }
+#if 0
+        if (mSettings->monitorFiles() && !monitor) {
+            // Settings have changed which might affect the alarm configuration
+            qCDebug(KALARMDIRRESOURCE_LOG) << "Monitored changed";
+            loadFiles(true);
+//              synchronizeCollectionTree();
+        }
+#endif
+    }
+
+    if (mSettings->updateStorageFormat()) {
+        // This is a flag to request that the backend calendar storage format should
+        // be updated to the current KAlarm format.
+        KACalendar::Compat okCompat(KACalendar::Current | KACalendar::Convertible);
+        if (mCompatibility & ~okCompat) {
+            qCWarning(KALARMDIRRESOURCE_LOG) << "Either incompatible storage format or nothing to update";
+        } else if (mSettings->readOnly()) {
+            qCWarning(KALARMDIRRESOURCE_LOG) << "Cannot update storage format for a read-only resource";
+        } else {
+            // Update the backend storage format to the current KAlarm format
+            bool ok = true;
+            for (QHash<QString, EventFile>::iterator it = mEvents.begin();  it != mEvents.end();  ++it) {
+                KAEvent &event = it.value().event;
+                if (event.compatibility() == KACalendar::Convertible) {
+                    if (writeToFile(event)) {
+                        event.setCompatibility(KACalendar::Current);
+                    } else {
+                        qCWarning(KALARMDIRRESOURCE_LOG) << "Error updating storage format for event id" << event.id();
+                        ok = false;
+                    }
+                }
+            }
+            if (ok) {
+                mCompatibility = KACalendar::Current;
+                mVersion       = KACalendar::CurrentFormat;
+                const Collection c(mCollectionId);
+                if (c.isValid()) {
+                    KAlarmResourceCommon::setCollectionCompatibility(c, mCompatibility, mVersion);
+                }
+            }
+        }
+        mSettings->setUpdateStorageFormat(false);
+        mSettings->save();
+    }
+}
+
+/******************************************************************************
+* Load and parse data from each file in the directory.
+* The events are cached in mEvents.
+*/
+bool KAlarmDirResource::loadFiles(bool sync)
+{
+    const QString dirPath = directoryName();
+    if (dirPath.isEmpty()) {
+        return false;
+    }
+    qCDebug(KALARMDIRRESOURCE_LOG) << dirPath;
+    const QDir dir(dirPath);
+
+    // Create the directory if it doesn't exist.
+    // This should only be needed for a new resource, but just in case ...
+    initializeDirectory();
+
+    mEvents.clear();
+    mFileEventIds.clear();
+
+    // Set the resource display name to the configured name, else the directory
+    // name, if not already set.
+    QString display = mSettings->displayName();
+    if (display.isEmpty()  && (name().isEmpty() || name() == identifier())) {
+        display = dir.dirName();
+    }
+    if (!display.isEmpty()) {
+        setName(display);
+    }
+
+    // Read and parse each file in turn
+    QDirIterator it(dir);
+    while (it.hasNext()) {
+        it.next();
+        const QString file = it.fileName();
+        if (isFileValid(file)) {
+            const QString path = filePath(file);
+            if (QFileInfo(path).isFile()) {
+                const KAEvent event = loadFile(path, file);
+                if (event.isValid()) {
+                    addEventFile(event, file);
+                    mFileEventIds.insert(file, event.id());
+                }
+            }
+        }
+    }
+    DEBUG_DATA;
+
+    setCompatibility(false);   // don't write compatibility - no collection exists yet
+
+    if (mSettings->monitorFiles()) {
+        // Monitor the directory for changes to the files
+        if (!KDirWatch::self()->contains(dirPath)) {
+            KDirWatch::self()->addDir(dirPath, KDirWatch::WatchFiles);
+        }
+    }
+
+    if (sync) {
+        // Ensure the Akonadi server is updated with the current list of events
+        synchronize();
+    }
+
+    Q_EMIT status(Idle);
+    return true;
+}
+
+/******************************************************************************
+* Load and parse data a single file in the directory.
+*/
+KAEvent KAlarmDirResource::loadFile(const QString &path, const QString &file)
+{
+    qCDebug(KALARMDIRRESOURCE_LOG) << path;
+    MemoryCalendar::Ptr calendar(new MemoryCalendar(QStringLiteral("UTC")));
+    FileStorage::Ptr fileStorage(new FileStorage(calendar, path, new ICalFormat()));
+    if (!fileStorage->load()) {
+        qCWarning(KALARMDIRRESOURCE_LOG) << "Error loading" << path;
+        return KAEvent();
+    }
+    const Event::List events = calendar->events();
+    if (events.isEmpty()) {
+        qCDebug(KALARMDIRRESOURCE_LOG) << "Empty calendar in file" << path;
+        return KAEvent();
+    }
+    if (events.count() > 1) {
+        qCWarning(KALARMDIRRESOURCE_LOG) << "Deleting" << events.count() - 1 << "excess events found in file" << path;
+        for (int i = 1;  i < events.count();  ++i) {
+            calendar->deleteEvent(events[i]);
+        }
+    }
+    const Event::Ptr kcalEvent(events[0]);
+    if (kcalEvent->uid() != file) {
+        qCWarning(KALARMDIRRESOURCE_LOG) << "File" << path << ": event id differs from file name";
+    }
+    if (kcalEvent->alarms().isEmpty()) {
+        qCWarning(KALARMDIRRESOURCE_LOG) << "File" << path << ": event contains no alarms";
+        return KAEvent();
+    }
+    // Convert event in memory to current KAlarm format if possible
+    int version;
+    KACalendar::Compat compat = KAlarmResourceCommon::getCompatibility(fileStorage, version);
+    KAEvent event(kcalEvent);
+    const QString mime = CalEvent::mimeType(event.category());
+    if (mime.isEmpty()) {
+        qCWarning(KALARMDIRRESOURCE_LOG) << "KAEvent has no usable alarms:" << event.id();
+        return KAEvent();
+    }
+    if (!mSettings->alarmTypes().contains(mime)) {
+        qCWarning(KALARMDIRRESOURCE_LOG) << "KAEvent has wrong alarm type for resource:" << mime;
+        return KAEvent();
+    }
+    event.setCompatibility(compat);
+    return event;
+}
+
+/******************************************************************************
+* After a file/event has been removed, load the next file in the list for the
+* event ID.
+* Reply = new event, or invalid if none.
+*/
+KAEvent KAlarmDirResource::loadNextFile(const QString &eventId, const QString &file)
+{
+    QString nextFile = file;
+    while (!nextFile.isEmpty()) {
+        // There is another file with the same ID - load it
+        const KAEvent event = loadFile(filePath(nextFile), nextFile);
+        if (event.isValid()) {
+            addEventFile(event, nextFile);
+            mFileEventIds.insert(nextFile, event.id());
+            return event;
+        }
+        mFileEventIds.remove(nextFile);
+        nextFile = removeEventFile(eventId, nextFile);
+    }
+    return KAEvent();
+}
+
+/******************************************************************************
+* Retrieve an event from the calendar, whose uid and Akonadi id are given by
+* 'item' (item.remoteId() and item.id() respectively).
+* Set the event into a new item's payload, and signal its retrieval by calling
+* itemRetrieved(newitem).
+*/
+bool KAlarmDirResource::retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &)
+{
+    const QString rid = item.remoteId();
+    QHash<QString, EventFile>::ConstIterator it = mEvents.constFind(rid);
+    if (it == mEvents.constEnd()) {
+        qCWarning(KALARMDIRRESOURCE_LOG) << "Event not found:" << rid;
+        Q_EMIT error(errorMessage(KAlarmResourceCommon::UidNotFound, rid));
+        return false;
+    }
+
+    KAEvent event(it.value().event);
+    const Item newItem = KAlarmResourceCommon::retrieveItem(item, event);
+    itemRetrieved(newItem);
+    return true;
+}
+
+/******************************************************************************
+* Called when an item has been added to the collection.
+* Store the event in a file, and set its Akonadi remote ID to the KAEvent's UID.
+*/
+void KAlarmDirResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &)
+{
+    qCDebug(KALARMDIRRESOURCE_LOG) << item.id();
+    if (cancelIfReadOnly()) {
+        return;
+    }
+
+    KAEvent event;
+    if (item.hasPayload<KAEvent>()) {
+        event = item.payload<KAEvent>();
+    }
+    if (!event.isValid()) {
+        changeProcessed();
+        return;
+    }
+    event.setCompatibility(KACalendar::Current);
+    setCompatibility();
+
+    if (!writeToFile(event)) {
+        return;
+    }
+
+    addEventFile(event, event.id());
+
+    Item newItem(item);
+    newItem.setRemoteId(event.id());
+//    scheduleWrite();    //???? is this needed?
+    changeCommitted(newItem);
+}
+
+/******************************************************************************
+* Called when an item has been changed.
+* Store the changed event in a file.
+*/
+void KAlarmDirResource::itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &)
+{
+    qCDebug(KALARMDIRRESOURCE_LOG) << item.id() << ", remote ID:" << item.remoteId();
+    if (cancelIfReadOnly()) {
+        return;
+    }
+    QHash<QString, EventFile>::iterator it = mEvents.find(item.remoteId());
+    if (it != mEvents.end()) {
+        if (it.value().event.isReadOnly()) {
+            qCWarning(KALARMDIRRESOURCE_LOG) << "Event is read only:" << item.remoteId();
+            cancelTask(errorMessage(KAlarmResourceCommon::EventReadOnly, item.remoteId()));
+            return;
+        }
+        if (it.value().event.compatibility() != KACalendar::Current) {
+            qCWarning(KALARMDIRRESOURCE_LOG) << "Event not in current format:" << item.remoteId();
+            cancelTask(errorMessage(KAlarmResourceCommon::EventNotCurrentFormat, item.remoteId()));
+            return;
+        }
+    }
+
+    KAEvent event;
+    if (item.hasPayload<KAEvent>()) {
+        event = item.payload<KAEvent>();
+    }
+    if (!event.isValid()) {
+        changeProcessed();
+        return;
+    }
+#if 0
+    QString errorMsg;
+    KAEvent event = KAlarmResourceCommon::checkItemChanged(item, errorMsg);
+    if (!event.isValid()) {
+        if (errorMsg.isEmpty()) {
+            changeProcessed();
+        } else {
+            cancelTask(errorMsg);
+        }
+        return;
+    }
+#endif
+    event.setCompatibility(KACalendar::Current);
+    if (mCompatibility != KACalendar::Current) {
+        setCompatibility();
+    }
+
+    if (!writeToFile(event)) {
+        return;
+    }
+
+    it.value().event = event;
+
+    changeCommitted(item);
+}
+
+/******************************************************************************
+* Called when an item has been deleted.
+* Delete the item's file.
+*/
+void KAlarmDirResource::itemRemoved(const Akonadi::Item &item)
+{
+    qCDebug(KALARMDIRRESOURCE_LOG) << item.id();
+    if (cancelIfReadOnly()) {
+        return;
+    }
+
+    QString nextFile;
+    removeEvent(item.remoteId(), true);
+    setCompatibility();
+    changeProcessed();
+}
+
+/******************************************************************************
+* Remove an event from the indexes, and optionally delete its file.
+*/
+void KAlarmDirResource::removeEvent(const QString &eventId, bool deleteFile)
+{
+    QString file = eventId;
+    QString nextFile;
+    QHash<QString, EventFile>::iterator it = mEvents.find(eventId);
+    if (it != mEvents.end()) {
+        file = it.value().files[0];
+        nextFile = removeEventFile(eventId, file);
+        mFileEventIds.remove(file);
+        DEBUG_DATA;
+    }
+    if (deleteFile) {
+        QFile::remove(filePath(file));
+    }
+
+    loadNextFile(eventId, nextFile);   // load any other file with the same event ID
+}
+
+/******************************************************************************
+* If the resource is read-only, cancel the task andQ_EMIT an error.
+* Reply = true if cancelled.
+*/
+bool KAlarmDirResource::cancelIfReadOnly()
+{
+    if (mSettings->readOnly()) {
+        qCWarning(KALARMDIRRESOURCE_LOG) << "Calendar is read-only:" << directoryName();
+        Q_EMIT error(i18nc("@info", "Trying to write to a read-only calendar: '%1'", directoryName()));
+        cancelTask();
+        return true;
+    }
+    return false;
+}
+
+/******************************************************************************
+* Write an event to a file. The file name is the event's id.
+*/
+bool KAlarmDirResource::writeToFile(const KAEvent &event)
+{
+    Event::Ptr kcalEvent(new Event);
+    event.updateKCalEvent(kcalEvent, KAEvent::UID_SET);
+    MemoryCalendar::Ptr calendar(new MemoryCalendar(QStringLiteral("UTC")));
+    KACalendar::setKAlarmVersion(calendar);   // set the KAlarm custom property
+    if (!calendar->addIncidence(kcalEvent)) {
+        qCritical() << "Error adding event with id" << event.id();
+        Q_EMIT error(errorMessage(KAlarmResourceCommon::CalendarAdd, event.id()));
+        cancelTask();
+        return false;
+    }
+
+    mChangedFiles += event.id();    // suppress KDirWatch processing for this write
+
+    const QString path = filePath(event.id());
+    qCDebug(KALARMDIRRESOURCE_LOG) << event.id() << " File:" << path;
+    FileStorage::Ptr fileStorage(new FileStorage(calendar, path, new ICalFormat()));
+    if (!fileStorage->save()) {
+        Q_EMIT error(i18nc("@info", "Failed to save event file: %1", path));
+        cancelTask();
+        return false;
+    }
+    return true;
+}
+
+/******************************************************************************
+* Create the resource's collection.
+*/
+void KAlarmDirResource::retrieveCollections()
+{
+    QString rid = mSettings->path();
+    if (!mCollectionFetched  &&  rid.isEmpty()) {
+        // The resource config seems to be missing. Execute this function
+        // once the collection config has been set up.
+        mWaitingToRetrieve = true;
+        return;
+    }
+
+    qCDebug(KALARMDIRRESOURCE_LOG);
+    Collection c;
+    c.setParentCollection(Collection::root());
+    c.setRemoteId(rid);
+    c.setContentMimeTypes(mSettings->alarmTypes());
+    setNameRights(c);
+
+    // Don't update CollectionAttribute here, since it hasn't yet been fetched
+    // from Akonadi database.
+
+    Collection::List list;
+    list << c;
+    collectionsRetrieved(list);
+}
+
+/******************************************************************************
+* Set the collection's name and rights.
+* It is the caller's responsibility to notify the Akonadi server.
+*/
+void KAlarmDirResource::setNameRights(Collection &c)
+{
+    qCDebug(KALARMDIRRESOURCE_LOG);
+    const QString display = mSettings->displayName();
+    c.setName(display.isEmpty() ? name() : display);
+    EntityDisplayAttribute *attr = c.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
+    attr->setDisplayName(name());
+    attr->setIconName(QStringLiteral("kalarm"));
+    if (mSettings->readOnly()) {
+        c.setRights(Collection::CanChangeCollection);
+    } else {
+        Collection::Rights rights = Collection::ReadOnly;
+        rights |= Collection::CanChangeItem;
+        rights |= Collection::CanCreateItem;
+        rights |= Collection::CanDeleteItem;
+        rights |= Collection::CanChangeCollection;
+        c.setRights(rights);
+    }
+    qCDebug(KALARMDIRRESOURCE_LOG) << "end";
+}
+
+/******************************************************************************
+* Retrieve all events from the directory, and set each into a new item's
+* payload. Items are identified by their remote IDs. The Akonadi ID is not
+* used.
+* Signal the retrieval of the items by calling itemsRetrieved(items), which
+* updates Akonadi with any changes to the items. itemsRetrieved() compares
+* the new and old items, matching them on the remoteId(). If the flags or
+* payload have changed, or the Item has any new Attributes, the Akonadi
+* storage is updated.
+*/
+void KAlarmDirResource::retrieveItems(const Akonadi::Collection &collection)
+{
+    mCollectionId = collection.id();   // note the one and only collection for this resource
+    qCDebug(KALARMDIRRESOURCE_LOG) << "Collection id:" << mCollectionId;
+
+    // Set the collection's compatibility status
+    KAlarmResourceCommon::setCollectionCompatibility(collection, mCompatibility, mVersion);
+
+    // Fetch the list of valid mime types
+    const QStringList mimeTypes = mSettings->alarmTypes();
+
+    // Retrieve events
+    Item::List items;
+    foreach (const EventFile &data, mEvents) {
+        const KAEvent &event = data.event;
+        const QString mime = CalEvent::mimeType(event.category());
+        if (mime.isEmpty()) {
+            qCWarning(KALARMDIRRESOURCE_LOG) << "KAEvent has no alarms:" << event.id();
+            continue;   // event has no usable alarms
+        }
+        if (!mimeTypes.contains(mime)) {
+            continue;    // restrict alarms returned to the defined types
+        }
+
+        Item item(mime);
+        item.setRemoteId(event.id());
+        item.setPayload(event);
+        items.append(item);
+    }
+
+    itemsRetrieved(items);
+}
+
+/******************************************************************************
+* Called when the collection has been changed.
+* Set its display name if that has changed.
+*/
+void KAlarmDirResource::collectionChanged(const Akonadi::Collection &collection)
+{
+    qCDebug(KALARMDIRRESOURCE_LOG);
+    // If the collection has a new display name, set the resource's display
+    // name the same, and save to the settings.
+    const QString newName = collection.displayName();
+    if (!newName.isEmpty()  &&  newName != name()) {
+        setName(newName);
+    }
+    if (newName != mSettings->displayName()) {
+        mSettings->setDisplayName(newName);
+        mSettings->save();
+    }
+
+    changeCommitted(collection);
+}
+
+/******************************************************************************
+* Called when a file has been created in the directory.
+*/
+void KAlarmDirResource::fileCreated(const QString &path)
+{
+    qCDebug(KALARMDIRRESOURCE_LOG) << path;
+    if (path == directoryName()) {
+        // The directory has been created. Load all files in it, and
+        // tell the Akonadi server to create an Item for each event.
+        loadFiles(true);
+        foreach (const EventFile &data, mEvents) {
+            createItem(data.event);
+        }
+    } else {
+        const QString file = fileName(path);
+        int i = mChangedFiles.indexOf(file);
+        if (i >= 0) {
+            mChangedFiles.removeAt(i);    // the file was updated by this resource
+        } else if (isFileValid(file)) {
+            if (createItemAndIndex(path, file)) {
+                setCompatibility();
+            }
+            DEBUG_DATA;
+        }
+    }
+}
+
+/******************************************************************************
+* Called when a file has changed in the directory.
+*/
+void KAlarmDirResource::fileChanged(const QString &path)
+{
+    if (path != directoryName()) {
+        qCDebug(KALARMDIRRESOURCE_LOG) << path;
+        const QString file = fileName(path);
+        int i = mChangedFiles.indexOf(file);
+        if (i >= 0) {
+            mChangedFiles.removeAt(i);    // the file was updated by this resource
+        } else if (isFileValid(file)) {
+            QString nextFile, oldId;
+            KAEvent oldEvent;
+            const KAEvent event = loadFile(path, file);
+            // Get the file's old event ID
+            QHash<QString, QString>::iterator fit = mFileEventIds.find(file);
+            if (fit != mFileEventIds.end()) {
+                oldId = fit.value();
+                if (event.id() != oldId) {
+                    // The file's event ID has changed - remove the old event
+                    nextFile = removeEventFile(oldId, file, &oldEvent);
+                    if (event.isValid()) {
+                        fit.value() = event.id();
+                    } else {
+                        mFileEventIds.erase(fit);
+                    }
+                }
+            } else if (event.isValid()) {
+                // The file didn't contain an event before. Save details of the new event.
+                mFileEventIds.insert(file, event.id());
+            }
+            addEventFile(event, file);
+
+            KAEvent e = loadNextFile(oldId, nextFile);   // load any other file with the same event ID
+            setCompatibility();
+
+            // Tell the Akonadi server to amend the Item for the event
+            if (event.id() != oldId) {
+                if (e.isValid()) {
+                    modifyItem(e);
+                } else {
+                    deleteItem(oldEvent);
+                }
+                createItem(event);   // create a new Item for the new event ID
+            } else {
+                modifyItem(event);
+            }
+            DEBUG_DATA;
+        }
+    }
+}
+
+/******************************************************************************
+* Called when a file has been deleted in the directory.
+*/
+void KAlarmDirResource::fileDeleted(const QString &path)
+{
+    qCDebug(KALARMDIRRESOURCE_LOG) << path;
+    if (path == directoryName()) {
+        // The directory has been deleted
+        mEvents.clear();
+        mFileEventIds.clear();
+
+        // Tell the Akonadi server to delete all Items in the collection
+        Collection c(mCollectionId);
+        ItemDeleteJob *job = new ItemDeleteJob(c);
+        connect(job, &CollectionFetchJob::result, this, &KAlarmDirResource::jobDone);
+    } else {
+        // A single file has been deleted
+        const QString file = fileName(path);
+        if (isFileValid(file)) {
+            QHash<QString, QString>::iterator fit = mFileEventIds.find(file);
+            if (fit != mFileEventIds.end()) {
+                QString eventId = fit.value();
+                KAEvent event;
+                QString nextFile = removeEventFile(eventId, file, &event);
+                mFileEventIds.erase(fit);
+
+                KAEvent e = loadNextFile(eventId, nextFile);   // load any other file with the same event ID
+                setCompatibility();
+
+                if (e.isValid()) {
+                    // Tell the Akonadi server to amend the Item for the event
+                    modifyItem(e);
+                } else {
+                    // Tell the Akonadi server to delete the Item for the event
+                    deleteItem(event);
+                }
+                DEBUG_DATA;
+            }
+        }
+    }
+}
+
+/******************************************************************************
+* Tell the Akonadi server to create an Item for a given file's event, and add
+* it to the indexes.
+*/
+bool KAlarmDirResource::createItemAndIndex(const QString &path, const QString &file)
+{
+    const KAEvent event = loadFile(path, file);
+    if (event.isValid()) {
+        // Tell the Akonadi server to create an Item for the event
+        if (createItem(event)) {
+            addEventFile(event, file);
+            mFileEventIds.insert(file, event.id());
+
+            return true;
+        }
+    }
+    return false;
+}
+
+/******************************************************************************
+* Tell the Akonadi server to create an Item for a given event.
+*/
+bool KAlarmDirResource::createItem(const KAEvent &event)
+{
+    Item item;
+    if (!event.setItemPayload(item, mSettings->alarmTypes())) {
+        qCWarning(KALARMDIRRESOURCE_LOG) << "Invalid mime type for collection";
+        return false;
+    }
+    Collection c(mCollectionId);
+    item.setParentCollection(c);
+    item.setRemoteId(event.id());
+    ItemCreateJob *job = new ItemCreateJob(item, c);
+    connect(job, &CollectionFetchJob::result, this, &KAlarmDirResource::jobDone);
+    return true;
+}
+
+/******************************************************************************
+* Tell the Akonadi server to amend the Item for a given event.
+*/
+bool KAlarmDirResource::modifyItem(const KAEvent &event)
+{
+    Item item;
+    if (!event.setItemPayload(item, mSettings->alarmTypes())) {
+        qCWarning(KALARMDIRRESOURCE_LOG) << "Invalid mime type for collection";
+        return false;
+    }
+    Collection c(mCollectionId);
+    item.setParentCollection(c);
+    item.setRemoteId(event.id());
+    ItemModifyJob *job = new ItemModifyJob(item);
+    job->disableRevisionCheck();
+    connect(job, &CollectionFetchJob::result, this, &KAlarmDirResource::jobDone);
+    return true;
+}
+
+/******************************************************************************
+* Tell the Akonadi server to delete the Item for a given event.
+*/
+void KAlarmDirResource::deleteItem(const KAEvent &event)
+{
+    Item item(CalEvent::mimeType(event.category()));
+    Collection c(mCollectionId);
+    item.setParentCollection(c);
+    item.setRemoteId(event.id());
+    ItemDeleteJob *job = new ItemDeleteJob(item);
+    connect(job, &CollectionFetchJob::result, this, &KAlarmDirResource::jobDone);
+}
+
+/******************************************************************************
+* Called when a collection or item job has completed.
+* Checks for any error.
+*/
+void KAlarmDirResource::jobDone(KJob *j)
+{
+    if (j->error()) {
+        qCritical() << j->metaObject()->className() << "error:" << j->errorString();
+    }
+}
+
+/******************************************************************************
+* Create the directory if it doesn't already exist, and ensure that it
+* contains a WARNING_README.txt file.
+*/
+void KAlarmDirResource::initializeDirectory() const
+{
+    qCDebug(KALARMDIRRESOURCE_LOG);
+    const QDir dir(directoryName());
+    const QString dirPath = dir.absolutePath();
+
+    // If folder does not exist, create it
+    if (!dir.exists()) {
+        qCDebug(KALARMDIRRESOURCE_LOG) << "Creating" << dirPath;
+        QDir::root().mkpath(dirPath);
+    }
+
+    // Check whether warning file is in place...
+    QFile file(dirPath + QDir::separator() + QLatin1String(warningFile));
+    if (!file.exists()) {
+        // ... if not, create it
+        file.open(QIODevice::WriteOnly);
+        file.write("Important Warning!!!\n"
+                   "Do not create or copy items inside this folder manually:\n"
+                   "they are managed by the Akonadi framework!\n");
+        file.close();
+    }
+}
+
+QString KAlarmDirResource::directoryName() const
+{
+    return mSettings->path();
+}
+
+QString KAlarmDirResource::filePath(const QString &file) const
+{
+    return mSettings->path() + QDir::separator() + file;
+}
+
+/******************************************************************************
+* Strip the directory path from a file name.
+*/
+QString KAlarmDirResource::fileName(const QString &path) const
+{
+    const QFileInfo fi(path);
+    if (fi.isDir()  ||  fi.isBundle()) {
+        return QString();
+    }
+    if (fi.path() == mSettings->path()) {
+        return fi.fileName();
+    }
+    return path;
+}
+
+/******************************************************************************
+* Evaluate the version compatibility status of the calendar. This is the OR of
+* the statuses of the individual events.
+*/
+void KAlarmDirResource::setCompatibility(bool writeAttr)
+{
+    static const KACalendar::Compat AllCompat(KACalendar::Current | KACalendar::Convertible | KACalendar::Incompatible);
+
+    const KACalendar::Compat oldCompatibility = mCompatibility;
+    const int oldVersion = mVersion;
+    if (mEvents.isEmpty()) {
+        mCompatibility = KACalendar::Current;
+    } else {
+        mCompatibility = KACalendar::Unknown;
+        foreach (const EventFile &data, mEvents) {
+            const KAEvent &event = data.event;
+            mCompatibility |= event.compatibility();
+            if ((mCompatibility & AllCompat) == AllCompat) {
+                break;
+            }
+        }
+    }
+    mVersion = (mCompatibility == KACalendar::Current) ? KACalendar::CurrentFormat : KACalendar::MixedFormat;
+    if (writeAttr  && (mCompatibility != oldCompatibility || mVersion != oldVersion)) {
+        const Collection c(mCollectionId);
+        if (c.isValid()) {
+            KAlarmResourceCommon::setCollectionCompatibility(c, mCompatibility, mVersion);
+        }
+    }
+}
+
+/******************************************************************************
+* Add an event/file combination to the mEvents map.
+*/
+void KAlarmDirResource::addEventFile(const KAEvent &event, const QString &file)
+{
+    if (event.isValid()) {
+        QHash<QString, EventFile>::iterator it = mEvents.find(event.id());
+        if (it != mEvents.end()) {
+            EventFile &data = it.value();
+            data.event = event;
+            data.files.removeAll(file);   // in case it isn't the first file
+            data.files.prepend(file);
+        } else {
+            mEvents.insert(event.id(), EventFile(event, QStringList(file)));
+        }
+    }
+}
+
+/******************************************************************************
+* Remove an event ID/file combination from the mEvents map.
+* Reply = next file with the same event ID.
+*/
+QString KAlarmDirResource::removeEventFile(const QString &eventId, const QString &file, KAEvent *event)
+{
+    QHash<QString, EventFile>::iterator it = mEvents.find(eventId);
+    if (it != mEvents.end()) {
+        if (event) {
+            *event = it.value().event;
+        }
+        it.value().files.removeAll(file);
+        if (!it.value().files.isEmpty()) {
+            return it.value().files[0];
+        }
+        mEvents.erase(it);
+    } else if (event) {
+        *event = KAEvent();
+    }
+    return QString();
+}
+
+/******************************************************************************
+* Check whether a file is to be ignored.
+* Reply = false if file is to be ignored.
+*/
+bool isFileValid(const QString &file)
+{
+    return !file.isEmpty()
+           &&  !file.startsWith(QLatin1Char('.'))  &&  !file.endsWith(QLatin1Char('~'))
+           &&  file != QLatin1String(warningFile);
+}
+
+AKONADI_RESOURCE_MAIN(KAlarmDirResource)
diff --git a/resources/kalarm/kalarmdir/kalarmdirresource.desktop b/resources/kalarm/kalarmdir/kalarmdirresource.desktop
new file mode 100644 (file)
index 0000000..386498f
--- /dev/null
@@ -0,0 +1,94 @@
+[Desktop Entry]
+Name=KAlarm Directory
+Name[bg]=Папка за KAlarm
+Name[bs]=KAlarm direktorij
+Name[ca]=Directori del KAlarm
+Name[ca@valencia]=Directori del KAlarm
+Name[cs]=Adresář KAlarmu
+Name[da]=KAlarm-mappe
+Name[de]=KAlarm-Ordner
+Name[el]=Κατάλογος KAlarm
+Name[en_GB]=KAlarm Directory
+Name[es]=Directorio de KAlarm
+Name[et]=KAlarmi kataloog
+Name[fi]=KAlarm-kansio
+Name[fr]=Dossier KAlarm
+Name[ga]=Comhadlann KAlarm
+Name[gl]=Directorio de KAlarm
+Name[hu]=KAlarm mappa
+Name[ia]=Directorio de KAlarm
+Name[it]=Directory KAlarm
+Name[kk]=KAlarm каталогын
+Name[km]=ថត KAlarm
+Name[ko]=KAlarm 디렉터리
+Name[lt]=KAlarm aplankas
+Name[lv]=KAlarm mape
+Name[nb]=KAlarn-mappe
+Name[nds]=KAlarm-Verteken
+Name[nl]=KAlarm-map
+Name[pl]=Katalog KAlarm
+Name[pt]=Pasta do KAlarm
+Name[pt_BR]=Pasta do KAlarm
+Name[ru]=Папка KAlarm
+Name[sk]=Adresár KAlarm
+Name[sl]=Mapa za KAlarm
+Name[sr]=К‑алармова фасцикла
+Name[sr@ijekavian]=К‑алармова фасцикла
+Name[sr@ijekavianlatin]=K‑alarmova fascikla
+Name[sr@latin]=K‑alarmova fascikla
+Name[sv]=Kalarm-katalog
+Name[tr]=KAlarm Dizini
+Name[uk]=Каталог KAlarm
+Name[x-test]=xxKAlarm Directoryxx
+Name[zh_CN]=KAlarm 目录
+Name[zh_TW]=KAlarm 目錄
+Comment=Loads data from a local KAlarm folder
+Comment[ast]=Carga datos dende una carpeta llocal de KAlarm
+Comment[bg]=Зареждане на данни от локална папка KAlarm
+Comment[bs]=Čita podatke iz lokalnog KAlarm direktorija
+Comment[ca]=Carrega les dades des d'una carpeta KAlarm local
+Comment[ca@valencia]=Carrega les dades des d'una carpeta KAlarm local
+Comment[cs]=Načítá data z místní složky KAlarmu
+Comment[da]=Indlæser data fra en lokal KAlarm-mappe
+Comment[de]=Laden von Daten aus einem lokalen KAlarm-Ordner
+Comment[el]=Φορτώνει δεδομένα από έναν τοπικό φάκελο KAlarm
+Comment[en_GB]=Loads data from a local KAlarm folder
+Comment[es]=Carga datos de una carpeta local de KAlarm
+Comment[et]=Andmete laadimine kohalikust KAlarmi kataloogist
+Comment[fi]=Lataa tietoja paikallisesta KAlarm-kansiosta
+Comment[fr]=Charge des données d'un dossier KAlarm
+Comment[ga]=Luchtaíonn sé sonraí ó fhillteán logánta KAlarm
+Comment[gl]=Carga datos desde un cartafol de KAlarm local.
+Comment[hu]=Adatok betöltése egy helyi KAlarm mappából
+Comment[ia]=Carga datos ex un dossier local de KAlarm
+Comment[it]=Carica dati da una cartella locale KAlarm
+Comment[kk]=Жергілікті KAlarm қапшығынан деректі алып береді
+Comment[km]=ផ្ទុក​ទិន្នន័យ​ពី​ថត KAlarm មូលដ្ឋាន
+Comment[ko]=로컬 KAlarm 폴더에서 데이터를 가져옵니다
+Comment[lt]=Įkelia duomenis iš vietinio KAlarm aplanko
+Comment[lv]=Ielādē datus no lokālās KAlarm mapes
+Comment[nb]=Laster data fra en lokal KAlarm-mappe
+Comment[nds]=Laadt Daten ut en lokaal KAlarm-Orner
+Comment[nl]=Laadt gegevens uit een lokale KAlarm-map
+Comment[pl]=Wczytuje dane z lokalnego katalogu KAlarm
+Comment[pt]=Carrega os dados de uma pasta local do KAlarm
+Comment[pt_BR]=Carrega os dados de uma pasta local do KAlarm
+Comment[ru]=Загрузка данных из локальной папки KAlarm
+Comment[sk]=Načíta dáta z miestneho priečinka KAlarm
+Comment[sl]=Naloži podatke iz krajevne mape za KAlarm
+Comment[sr]=Учитава податке из локалне К‑алармове фасцикле
+Comment[sr@ijekavian]=Учитава податке из локалне К‑алармове фасцикле
+Comment[sr@ijekavianlatin]=Učitava podatke iz lokalne K‑alarmove fascikle
+Comment[sr@latin]=Učitava podatke iz lokalne K‑alarmove fascikle
+Comment[sv]=Laddar data från en lokal Kalarm-katalog
+Comment[tr]=Yerel KAlarm dizininden veri yükleme aracı
+Comment[uk]=Завантажує дані з локальної теки KAlarm
+Comment[x-test]=xxLoads data from a local KAlarm folderxx
+Comment[zh_CN]=从本地 KAlarm 文件夹载入数据
+Comment[zh_TW]=從本地 KAlarm 目錄中載入資料
+Type=AkonadiResource
+Exec=akonadi_kalarm_dir_resource
+Icon=kalarm
+X-Akonadi-MimeTypes=application/x-vnd.kde.alarm,application/x-vnd.kde.alarm.active,application/x-vnd.kde.alarm.archived,application/x-vnd.kde.alarm.template
+X-Akonadi-Capabilities=Resource
+X-Akonadi-Identifier=akonadi_kalarm_dir_resource
diff --git a/resources/kalarm/kalarmdir/kalarmdirresource.h b/resources/kalarm/kalarmdir/kalarmdirresource.h
new file mode 100644 (file)
index 0000000..33aba01
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ *  kalarmdirresource.h  -  Akonadi directory resource for KAlarm
+ *  Program:  kalarm
+ *  Copyright © 2011-2012 by David Jarvie <djarvie@kde.org>
+ *
+ *  This library is free software; you can redistribute it and/or modify it
+ *  under the terms of the GNU Library General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or (at your
+ *  option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful, but WITHOUT
+ *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ *  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ *  License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public License
+ *  along with this library; see the file COPYING.LIB.  If not, write to the
+ *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ *  02110-1301, USA.
+ */
+
+#ifndef KALARMDIRRESOURCE_H
+#define KALARMDIRRESOURCE_H
+
+#include <kalarmcal/kaevent.h>
+
+#include <resourcebase.h>
+#include <QHash>
+
+namespace Akonadi_KAlarm_Dir_Resource
+{
+class Settings;
+}
+using namespace KAlarmCal;
+
+class KAlarmDirResource : public Akonadi::ResourceBase, public Akonadi::AgentBase::Observer
+{
+    Q_OBJECT
+public:
+    explicit KAlarmDirResource(const QString &id);
+    ~KAlarmDirResource();
+
+public Q_SLOTS:
+    void configure(WId windowId) Q_DECL_OVERRIDE;
+    void aboutToQuit() Q_DECL_OVERRIDE;
+
+protected Q_SLOTS:
+    void retrieveCollections() Q_DECL_OVERRIDE;
+    void retrieveItems(const Akonadi::Collection &) Q_DECL_OVERRIDE;
+    bool retrieveItem(const Akonadi::Item &, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+
+protected:
+    void collectionChanged(const Akonadi::Collection &) Q_DECL_OVERRIDE;
+    void itemAdded(const Akonadi::Item &, const Akonadi::Collection &) Q_DECL_OVERRIDE;
+    void itemChanged(const Akonadi::Item &, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+    void itemRemoved(const Akonadi::Item &) Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void    settingsChanged();
+    void    fileCreated(const QString &path);
+    void    fileChanged(const QString &path);
+    void    fileDeleted(const QString &path);
+    void    loadFiles()
+    {
+        loadFiles(true);
+    }
+    void    collectionFetchResult(KJob *);
+    void    jobDone(KJob *);
+
+private:
+    void   changeAlarmTypes(CalEvent::Types removed);
+    bool    loadFiles(bool sync);
+    KAEvent loadFile(const QString &path, const QString &file);
+    KAEvent loadNextFile(const QString &eventId, const QString &file);
+    QString directoryName() const;
+    QString filePath(const QString &file) const;
+    QString fileName(const QString &path) const;
+    void    initializeDirectory() const;
+    void    setNameRights(Akonadi::Collection &);
+    bool    cancelIfReadOnly();
+    bool    writeToFile(const KAEvent &);
+    void    setCompatibility(bool writeAttr = true);
+    void    removeEvent(const QString &eventId, bool deleteFile);
+    void    addEventFile(const KAEvent &, const QString &file);
+    QString removeEventFile(const QString &eventId, const QString &file, KAEvent * = Q_NULLPTR);
+    bool    createItemAndIndex(const QString &path, const QString &file);
+    bool    createItem(const KAEvent &);
+    bool    modifyItem(const KAEvent &);
+    void    deleteItem(const KAEvent &);
+
+    struct EventFile {  // data to be indexed by event ID
+        EventFile() {}
+        EventFile(const KAEvent &e, const QStringList &f) : event(e), files(f) {}
+        KAEvent     event;
+        QStringList files;   // files containing this event ID, in-use one first
+    };
+    QHash<QString, EventFile> mEvents;         // cached alarms and file names, indexed by ID
+    QHash<QString, QString>   mFileEventIds;   // alarm IDs, indexed by file name
+    Akonadi_KAlarm_Dir_Resource::Settings *mSettings;
+    Akonadi::Collection::Id   mCollectionId;   // ID of this resource's collection
+    KACalendar::Compat        mCompatibility;
+    int                       mVersion;        // calendar format version
+    QStringList               mChangedFiles;   // files being written to
+    bool                      mCollectionFetched;  // mCollectionId has been initialised
+    bool                      mWaitingToRetrieve;  // retrieveCollections() needs to be called
+};
+
+#endif
+
diff --git a/resources/kalarm/kalarmdir/kalarmdirresource.kcfg b/resources/kalarm/kalarmdir/kalarmdirresource.kcfg
new file mode 100644 (file)
index 0000000..3252a7d
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
+      http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
+  <kcfgfile arg="true" />
+  <group name="General">
+    <entry name="Path" type="Path">
+      <label>Path to KAlarm directory.</label>
+      <default></default>
+    </entry>
+    <entry name="DisplayName" type="String">
+      <label>Display name.</label>
+      <default></default>
+    </entry>
+    <entry name="ReadOnly" type="Bool">
+      <label>Do not change the actual backend data.</label>
+      <default>false</default>
+    </entry>
+    <entry name="MonitorFiles" type="Bool">
+      <label>Monitor directory for changes.</label>
+      <default>true</default>
+    </entry>
+    <entry name="AlarmTypes" type="StringList">
+      <label>Alarm types.</label>
+    </entry>
+    <entry name="UpdateStorageFormat" type="Bool">
+      <label>Update backend storage format.</label>
+      <default>false</default>
+    </entry>
+  </group>
+</kcfg>
diff --git a/resources/kalarm/kalarmdir/settings.kcfgc b/resources/kalarm/kalarmdir/settings.kcfgc
new file mode 100644 (file)
index 0000000..9a5e74d
--- /dev/null
@@ -0,0 +1,8 @@
+File=kalarmdirresource.kcfg
+ClassName=Settings
+Mutators=true
+ItemAccessors=true
+SetUserTexts=true
+Singleton=false
+GlobalEnums=true
+NameSpace=Akonadi_KAlarm_Dir_Resource
diff --git a/resources/kalarm/kalarmdir/settingsdialog.cpp b/resources/kalarm/kalarmdir/settingsdialog.cpp
new file mode 100644 (file)
index 0000000..f04c2d0
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+ *  settingsdialog.cpp  -  Akonadi KAlarm directory resource configuration dialog
+ *  Program:  kalarm
+ *  Copyright © 2011 by David Jarvie <djarvie@kde.org>
+ *
+ *  This library is free software; you can redistribute it and/or modify it
+ *  under the terms of the GNU Library General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or (at your
+ *  option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful, but WITHOUT
+ *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ *  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ *  License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public License
+ *  along with this library; see the file COPYING.LIB.  If not, write to the
+ *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ *  02110-1301, USA.
+ */
+
+#include "settingsdialog.h"
+#include "settings.h"
+#include "alarmtypewidget.h"
+
+#include <KConfigDialogManager>
+#include <KWindowSystem>
+
+#include <QUrl>
+#include <QTimer>
+#include <QDialogButtonBox>
+#include <QPushButton>
+#include <QVBoxLayout>
+
+namespace Akonadi_KAlarm_Dir_Resource
+{
+
+SettingsDialog::SettingsDialog(WId windowId, Settings *settings)
+    : QDialog(),
+      mSettings(settings),
+      mReadOnlySelected(false)
+{
+    QWidget *mainWidget = new QWidget(this);
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    setLayout(mainLayout);
+    mainLayout->addWidget(mainWidget);
+    ui.setupUi(mainWidget);
+    mTypeSelector = new AlarmTypeWidget(ui.tab, ui.tabLayout);
+    ui.ktabwidget->tabBar()->hide();
+    ui.kcfg_Path->setMode(KFile::LocalOnly | KFile::Directory);
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    mOkButton = buttonBox->button(QDialogButtonBox::Ok);
+    mOkButton->setDefault(true);
+    mOkButton->setShortcut(Qt::CTRL | Qt::Key_Return);
+    connect(buttonBox, &QDialogButtonBox::accepted, this, &SettingsDialog::accept);
+    connect(buttonBox, &QDialogButtonBox::rejected, this, &SettingsDialog::reject);
+    mainLayout->addWidget(buttonBox);
+    setWindowTitle(i18nc("@title", "Configure Calendar"));
+
+    if (windowId) {
+        KWindowSystem::setMainWindow(this, windowId);
+    }
+
+    // Make directory path read-only if the resource already exists
+    QUrl path = QUrl::fromLocalFile(mSettings->path());
+    ui.kcfg_Path->setUrl(path);
+    if (!path.isEmpty()) {
+        ui.kcfg_Path->setEnabled(false);
+    }
+
+    mTypeSelector->setAlarmTypes(CalEvent::types(mSettings->alarmTypes()));
+    mManager = new KConfigDialogManager(this, mSettings);
+    mManager->updateWidgets();
+
+    connect(mOkButton, &QPushButton::clicked, this, &SettingsDialog::save);
+    connect(ui.kcfg_Path, &KUrlRequester::textChanged, this, &SettingsDialog::textChanged);
+    connect(ui.kcfg_ReadOnly, &QCheckBox::clicked, this, &SettingsDialog::readOnlyClicked);
+    connect(mTypeSelector, &AlarmTypeWidget::changed, this, &SettingsDialog::validate);
+
+    QTimer::singleShot(0, this, &SettingsDialog::validate);
+}
+
+void SettingsDialog::save()
+{
+    mManager->updateSettings();
+    mSettings->setPath(ui.kcfg_Path->url().toLocalFile());
+    mSettings->setAlarmTypes(CalEvent::mimeTypes(mTypeSelector->alarmTypes()));
+    mSettings->save();
+}
+
+void SettingsDialog::readOnlyClicked(bool set)
+{
+    mReadOnlySelected = set;
+}
+
+void SettingsDialog::textChanged()
+{
+    bool oldReadOnly = ui.kcfg_ReadOnly->isEnabled();
+    validate();
+    if (ui.kcfg_ReadOnly->isEnabled()  &&  !oldReadOnly) {
+        // If read-only was only set earlier by validating the input path,
+        // and the path is now ok to be read-write, clear the read-only status.
+        ui.kcfg_ReadOnly->setChecked(mReadOnlySelected);
+    }
+}
+
+void SettingsDialog::validate()
+{
+    bool enableOk = false;
+    // At least one alarm type must be selected
+    if (mTypeSelector->alarmTypes() != CalEvent::EMPTY) {
+        // The entered URL must be valid and local
+        const QUrl currentUrl = ui.kcfg_Path->url();
+        if (currentUrl.isEmpty()) {
+            ui.kcfg_ReadOnly->setEnabled(true);
+        } else if (currentUrl.isLocalFile()) {
+            QFileInfo file(currentUrl.toLocalFile());
+            // It must either be an existing directory, or in a writable
+            // directory, and it must not be an existing file.
+            if (file.exists()) {
+                if (file.isDir()) {
+                    enableOk = true;    // it's an existing directory
+                }
+            } else {
+                // Specified directory doesn't already exist.
+                // Find the first level of parent directory which exists,
+                // and check that it is writable.
+                for (;;) {
+                    file.setFile(file.dir().absolutePath());   // get parent dir's file info
+                    if (file.exists()) {
+                        if (file.isDir()  &&  file.isWritable()) {
+                            enableOk = true;    // it's possible to create the directory
+                        }
+                        break;
+                    }
+                }
+            }
+        }
+    }
+    mOkButton->setEnabled(enableOk);
+}
+
+}
+
diff --git a/resources/kalarm/kalarmdir/settingsdialog.h b/resources/kalarm/kalarmdir/settingsdialog.h
new file mode 100644 (file)
index 0000000..3a141a9
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ *  settingsdialog.h  -  Akonadi KAlarm directory resource configuration dialog
+ *  Program:  kalarm
+ *  Copyright © 2011 by David Jarvie <djarvie@kde.org>
+ *
+ *  This library is free software; you can redistribute it and/or modify it
+ *  under the terms of the GNU Library General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or (at your
+ *  option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful, but WITHOUT
+ *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ *  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ *  License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public License
+ *  along with this library; see the file COPYING.LIB.  If not, write to the
+ *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ *  02110-1301, USA.
+ */
+
+#ifndef KALARMDIR_SETTINGSDIALOG_H
+#define KALARMDIR_SETTINGSDIALOG_H
+
+#include "ui_settingsdialog.h"
+#include "ui_alarmtypewidget.h"
+
+#include <kalarmcal/kacalendar.h>
+
+#include <QDialog>
+class QPushButton;
+using namespace KAlarmCal;
+
+class KConfigDialogManager;
+class AlarmTypeWidget;
+
+namespace Akonadi_KAlarm_Dir_Resource
+{
+
+class Settings;
+
+class SettingsDialog : public QDialog
+{
+    Q_OBJECT
+public:
+    SettingsDialog(WId windowId, Settings *);
+    void setAlarmTypes(CalEvent::Types);
+    CalEvent::Types alarmTypes() const;
+
+private Q_SLOTS:
+    void save();
+    void validate();
+    void textChanged();
+    void readOnlyClicked(bool);
+
+private:
+    Ui::SettingsDialog    ui;
+    AlarmTypeWidget      *mTypeSelector;
+    QPushButton          *mOkButton;
+    KConfigDialogManager *mManager;
+    Akonadi_KAlarm_Dir_Resource::Settings *mSettings;
+    bool                  mReadOnlySelected;   // read-only was set by user (not by validate())
+};
+
+}
+
+#endif // KALARMDIR_SETTINGSDIALOG_H
+
diff --git a/resources/kalarm/kalarmdir/settingsdialog.ui b/resources/kalarm/kalarmdir/settingsdialog.ui
new file mode 100644 (file)
index 0000000..3b4f6f1
--- /dev/null
@@ -0,0 +1,162 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SettingsDialog</class>
+ <widget class="QWidget" name="SettingsDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>320</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>400</width>
+    <height>10</height>
+   </size>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QTabWidget" name="ktabwidget">
+     <property name="currentIndex">
+      <number>0</number>
+     </property>
+     <widget class="QWidget" name="tab">
+      <attribute name="title">
+       <string>Directory</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="tabLayout">
+       <item>
+        <widget class="QGroupBox" name="groupBox_2">
+         <property name="title">
+          <string>Directory Name</string>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_3">
+          <item>
+           <layout class="QHBoxLayout" name="horizontalLayout">
+            <item>
+             <widget class="QLabel" name="label">
+              <property name="text">
+               <string>Director&amp;y:</string>
+              </property>
+              <property name="buddy">
+               <cstring>kcfg_Path</cstring>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="KUrlRequester" name="kcfg_Path"/>
+            </item>
+           </layout>
+          </item>
+          <item>
+           <widget class="QLabel" name="label_3">
+            <property name="text">
+             <string>Select the directory whose contents should be represented by this resource. If the directory does not exist, it will be created.</string>
+            </property>
+            <property name="wordWrap">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <widget class="QGroupBox" name="groupBox_3">
+         <property name="title">
+          <string>Display Name</string>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_1">
+          <item>
+           <layout class="QHBoxLayout" name="horizontalLayout_2">
+            <item>
+             <widget class="QLabel" name="label_1">
+              <property name="text">
+               <string>&amp;Name:</string>
+              </property>
+              <property name="buddy">
+               <cstring>kcfg_DisplayName</cstring>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="KLineEdit" name="kcfg_DisplayName"/>
+            </item>
+           </layout>
+          </item>
+          <item>
+           <widget class="QLabel" name="label_5">
+            <property name="text">
+             <string>Enter the name used to identify this resource in displays. If not specified, the directory name will be used.</string>
+            </property>
+            <property name="wordWrap">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <widget class="QGroupBox" name="groupBox">
+         <property name="title">
+          <string>Access Rights</string>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_2">
+          <item>
+           <widget class="QCheckBox" name="kcfg_ReadOnly">
+            <property name="text">
+             <string>Read only</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QLabel" name="label_2">
+            <property name="text">
+             <string>If read-only mode is enabled, no changes will be written to the directory selected above. Read-only mode will be automatically enabled if you do not have write access to the directory.</string>
+            </property>
+            <property name="wordWrap">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KUrlRequester</class>
+   <extends>QFrame</extends>
+   <header>kurlrequester.h</header>
+   <container>1</container>
+  </customwidget>
+  <customwidget>
+   <class>KLineEdit</class>
+   <extends>QLineEdit</extends>
+   <header>klineedit.h</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/resources/kalarm/shared/alarmtyperadiowidget.cpp b/resources/kalarm/shared/alarmtyperadiowidget.cpp
new file mode 100644 (file)
index 0000000..b5954f9
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ *  alarmtyperadiowidget.cpp  -  KAlarm alarm type exclusive selection widget
+ *  Program:  kalarm
+ *  Copyright © 2011 by David Jarvie <djarvie@kde.org>
+ *
+ *  This library is free software; you can redistribute it and/or modify it
+ *  under the terms of the GNU Library General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or (at your
+ *  option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful, but WITHOUT
+ *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ *  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ *  License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public License
+ *  along with this library; see the file COPYING.LIB.  If not, write to the
+ *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ *  02110-1301, USA.
+ */
+
+#include "alarmtyperadiowidget.h"
+
+AlarmTypeRadioWidget::AlarmTypeRadioWidget(QWidget *parent)
+    : Akonadi::SingleFileValidatingWidget(parent)
+{
+    ui.setupUi(this);
+    ui.mainLayout->setContentsMargins(0, 0, 0, 0);
+    mButtonGroup = new QButtonGroup(ui.groupBox);
+    mButtonGroup->addButton(ui.activeRadio);
+    mButtonGroup->addButton(ui.archivedRadio);
+    mButtonGroup->addButton(ui.templateRadio);
+    connect(ui.activeRadio, &QRadioButton::toggled, this, &AlarmTypeRadioWidget::changed);
+    connect(ui.archivedRadio, &QRadioButton::toggled, this, &AlarmTypeRadioWidget::changed);
+    connect(ui.templateRadio, &QRadioButton::toggled, this, &AlarmTypeRadioWidget::changed);
+}
+
+void AlarmTypeRadioWidget::setAlarmType(CalEvent::Type type)
+{
+    switch (type) {
+    case CalEvent::ACTIVE:
+        ui.activeRadio->setChecked(true);
+        break;
+    case CalEvent::ARCHIVED:
+        ui.archivedRadio->setChecked(true);
+        break;
+    case CalEvent::TEMPLATE:
+        ui.templateRadio->setChecked(true);
+        break;
+    default:
+        break;
+    }
+}
+
+CalEvent::Type AlarmTypeRadioWidget::alarmType() const
+{
+    if (ui.activeRadio->isChecked()) {
+        return CalEvent::ACTIVE;
+    }
+    if (ui.archivedRadio->isChecked()) {
+        return CalEvent::ARCHIVED;
+    }
+    if (ui.templateRadio->isChecked()) {
+        return CalEvent::TEMPLATE;
+    }
+    return CalEvent::EMPTY;
+}
+
+bool AlarmTypeRadioWidget::validate() const
+{
+    return static_cast<bool>(mButtonGroup->checkedButton());
+}
+
diff --git a/resources/kalarm/shared/alarmtyperadiowidget.h b/resources/kalarm/shared/alarmtyperadiowidget.h
new file mode 100644 (file)
index 0000000..94f6b4e
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ *  alarmtyperadiowidget.h  -  KAlarm alarm type exclusive selection widget
+ *  Program:  kalarm
+ *  Copyright © 2011 by David Jarvie <djarvie@kde.org>
+ *
+ *  This library is free software; you can redistribute it and/or modify it
+ *  under the terms of the GNU Library General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or (at your
+ *  option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful, but WITHOUT
+ *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ *  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ *  License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public License
+ *  along with this library; see the file COPYING.LIB.  If not, write to the
+ *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ *  02110-1301, USA.
+ */
+
+#ifndef ALARMTYPERADIOWIDGET_H
+#define ALARMTYPERADIOWIDGET_H
+
+#include "singlefileresourceconfigdialogbase.h"
+#include "ui_alarmtyperadiowidget.h"
+
+#include <kalarmcal/kacalendar.h>
+
+using namespace KAlarmCal;
+
+class QButtonGroup;
+
+class AlarmTypeRadioWidget : public Akonadi::SingleFileValidatingWidget
+{
+    Q_OBJECT
+public:
+    explicit AlarmTypeRadioWidget(QWidget *parent = Q_NULLPTR);
+    void setAlarmType(CalEvent::Type);
+    CalEvent::Type alarmType() const;
+    bool validate() const Q_DECL_OVERRIDE;
+
+private:
+    Ui::AlarmTypeRadioWidget ui;
+    QButtonGroup *mButtonGroup;
+};
+
+#endif // ALARMTYPERADIOWIDGET_H
+
diff --git a/resources/kalarm/shared/alarmtyperadiowidget.ui b/resources/kalarm/shared/alarmtyperadiowidget.ui
new file mode 100644 (file)
index 0000000..1af6374
--- /dev/null
@@ -0,0 +1,50 @@
+<ui version="4.0" >
+ <class>AlarmTypeRadioWidget</class>
+  <widget class="QWidget" name="AlarmTypeRadioWidget">
+   <layout class="QVBoxLayout" name="mainLayout">
+    <item>
+  <widget class="QGroupBox" name="groupBox">
+   <property name="title" >
+    <string>Alarm Types</string>
+   </property>
+   <property name="whatsThis">
+    <string>Select which alarm type this resource should contain.</string>
+   </property>
+   <layout class="QHBoxLayout" name="layout">
+    <item>
+     <widget class="QRadioButton" name="activeRadio">
+      <property name="text" >
+       <string>Active Alarms</string>
+      </property>
+      <property name="checked">
+       <bool>false</bool>
+      </property>
+     </widget>
+    </item>
+    <item>
+     <widget class="QRadioButton" name="archivedRadio">
+      <property name="text">
+       <string>Archived Alarms</string>
+      </property>
+      <property name="checked">
+       <bool>false</bool>
+      </property>
+     </widget>
+    </item>
+    <item>
+     <widget class="QRadioButton" name="templateRadio">
+      <property name="text">
+       <string>Alarm Templates</string>
+      </property>
+      <property name="checked">
+       <bool>false</bool>
+      </property>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+    </item>
+   </layout>
+  </widget>
+ <resources/>
+</ui>
diff --git a/resources/kalarm/shared/alarmtypewidget.cpp b/resources/kalarm/shared/alarmtypewidget.cpp
new file mode 100644 (file)
index 0000000..825f4f8
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ *  alarmtypewidget.cpp  -  KAlarm Akonadi configuration alarm type selection widget
+ *  Program:  kalarm
+ *  Copyright © 2011 by David Jarvie <djarvie@kde.org>
+ *
+ *  This library is free software; you can redistribute it and/or modify it
+ *  under the terms of the GNU Library General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or (at your
+ *  option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful, but WITHOUT
+ *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ *  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ *  License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public License
+ *  along with this library; see the file COPYING.LIB.  If not, write to the
+ *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ *  02110-1301, USA.
+ */
+
+#include "alarmtypewidget.h"
+
+AlarmTypeWidget::AlarmTypeWidget(QWidget *parent, QLayout *layout)
+    : QWidget()
+{
+    ui.setupUi(parent);
+    layout->addWidget(ui.groupBox);
+    connect(ui.activeCheckBox, &QCheckBox::toggled, this, &AlarmTypeWidget::changed);
+    connect(ui.archivedCheckBox, &QCheckBox::toggled, this, &AlarmTypeWidget::changed);
+    connect(ui.templateCheckBox, &QCheckBox::toggled, this, &AlarmTypeWidget::changed);
+}
+
+void AlarmTypeWidget::setAlarmTypes(CalEvent::Types types)
+{
+    if (types & CalEvent::ACTIVE) {
+        ui.activeCheckBox->setChecked(true);
+    }
+    if (types & CalEvent::ARCHIVED) {
+        ui.archivedCheckBox->setChecked(true);
+    }
+    if (types & CalEvent::TEMPLATE) {
+        ui.templateCheckBox->setChecked(true);
+    }
+}
+
+CalEvent::Types AlarmTypeWidget::alarmTypes() const
+{
+    CalEvent::Types types = CalEvent::EMPTY;
+    if (ui.activeCheckBox->isChecked()) {
+        types |= CalEvent::ACTIVE;
+    }
+    if (ui.archivedCheckBox->isChecked()) {
+        types |= CalEvent::ARCHIVED;
+    }
+    if (ui.templateCheckBox->isChecked()) {
+        types |= CalEvent::TEMPLATE;
+    }
+    return types;
+}
+
diff --git a/resources/kalarm/shared/alarmtypewidget.h b/resources/kalarm/shared/alarmtypewidget.h
new file mode 100644 (file)
index 0000000..ffc4566
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ *  alarmtypewidget.h  -  KAlarm Akonadi configuration alarm type selection widget
+ *  Program:  kalarm
+ *  Copyright © 2011 by David Jarvie <djarvie@kde.org>
+ *
+ *  This library is free software; you can redistribute it and/or modify it
+ *  under the terms of the GNU Library General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or (at your
+ *  option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful, but WITHOUT
+ *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ *  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ *  License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public License
+ *  along with this library; see the file COPYING.LIB.  If not, write to the
+ *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ *  02110-1301, USA.
+ */
+
+#ifndef ALARMTYPEWIDGET_H
+#define ALARMTYPEWIDGET_H
+
+#include "ui_alarmtypewidget.h"
+
+#include <kalarmcal/kacalendar.h>
+
+using namespace KAlarmCal;
+
+class AlarmTypeWidget : public QWidget
+{
+    Q_OBJECT
+public:
+    AlarmTypeWidget(QWidget *parent, QLayout *layout);
+    void setAlarmTypes(CalEvent::Types);
+    CalEvent::Types alarmTypes() const;
+
+Q_SIGNALS:
+    void changed();
+
+private:
+    Ui::AlarmTypeWidget ui;
+};
+
+#endif // ALARMTYPEWIDGET_H
+
diff --git a/resources/kalarm/shared/alarmtypewidget.ui b/resources/kalarm/shared/alarmtypewidget.ui
new file mode 100644 (file)
index 0000000..a767a11
--- /dev/null
@@ -0,0 +1,50 @@
+<ui version="4.0" >
+ <class>AlarmTypeWidget</class>
+ <widget class="QWidget" name="AlarmTypeWidget">
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QGroupBox" name="groupBox">
+     <property name="title" >
+      <string>Alarm Types</string>
+     </property>
+     <property name="whatsThis">
+      <string>Select which alarm type(s) this resource should contain.</string>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout_1">
+      <item>
+       <widget class="QCheckBox" name="activeCheckBox">
+        <property name="text" >
+         <string>Active Alarms</string>
+        </property>
+        <property name="checked">
+         <bool>false</bool>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QCheckBox" name="archivedCheckBox">
+        <property name="text">
+         <string>Archived Alarms</string>
+        </property>
+        <property name="checked">
+         <bool>false</bool>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QCheckBox" name="templateCheckBox">
+        <property name="text">
+         <string>Alarm Templates</string>
+        </property>
+        <property name="checked">
+         <bool>false</bool>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+</ui>
diff --git a/resources/kalarm/shared/kalarmresourcecommon.cpp b/resources/kalarm/shared/kalarmresourcecommon.cpp
new file mode 100644 (file)
index 0000000..7564bb8
--- /dev/null
@@ -0,0 +1,200 @@
+/*
+ *  kalarmresourcecommon.cpp  -  common functions for KAlarm Akonadi resources
+ *  Program:  kalarm
+ *  Copyright © 2009-2014 by David Jarvie <djarvie@kde.org>
+ *
+ *  This library is free software; you can redistribute it and/or modify it
+ *  under the terms of the GNU Library General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or (at your
+ *  option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful, but WITHOUT
+ *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ *  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ *  License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public License
+ *  along with this library; see the file COPYING.LIB.  If not, write to the
+ *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ *  02110-1301, USA.
+ */
+
+#include "kalarmresourcecommon.h"
+
+#include <kalarmcal/compatibilityattribute.h>
+#include <kalarmcal/eventattribute.h>
+
+#include <attributefactory.h>
+#include <collectionmodifyjob.h>
+
+#include <KCalCore/FileStorage>
+#include <KCalCore/MemoryCalendar>
+
+#include <KLocalizedString>
+
+#include <QTime>
+#include <qdebug.h>
+
+using namespace Akonadi;
+using namespace KCalCore;
+using namespace KAlarmCal;
+
+class Private : public QObject
+{
+    Q_OBJECT
+public:
+    Private(QObject *parent) : QObject(parent) {}
+    static Private *mInstance;
+
+private Q_SLOTS:
+    void modifyCollectionJobDone(KJob *);
+};
+Private *Private::mInstance = Q_NULLPTR;
+
+namespace KAlarmResourceCommon
+{
+
+/******************************************************************************
+* Perform common initialisation for KAlarm resources.
+*/
+void initialise(QObject *parent)
+{
+    // Create an object which can receive signals.
+    if (!Private::mInstance) {
+        Private::mInstance = new Private(parent);
+    }
+
+    // Set a default start-of-day time for date-only alarms.
+    KAEvent::setStartOfDay(QTime(0, 0, 0));
+
+    AttributeFactory::registerAttribute<CompatibilityAttribute>();
+    AttributeFactory::registerAttribute<EventAttribute>();
+}
+
+/******************************************************************************
+* Find the compatibility of an existing calendar file, and convert it in
+* memory to the current KAlarm format (if possible).
+*/
+KACalendar::Compat getCompatibility(const FileStorage::Ptr &fileStorage, int &version)
+{
+    QString versionString;
+    version = KACalendar::updateVersion(fileStorage, versionString);
+    switch (version) {
+    case KACalendar::IncompatibleFormat:
+        return KACalendar::Incompatible;  // calendar is not in KAlarm format, or is in a future format
+    case KACalendar::CurrentFormat:
+        return KACalendar::Current;       // calendar is in the current format
+    default:
+        return KACalendar::Convertible;   // calendar is in an out of date format
+    }
+}
+
+/******************************************************************************
+* Set an event into a new item's payload and return the new item.
+* The caller should signal its retrieval by calling itemRetrieved(newitem).
+* NOTE: the caller must set the event's compatibility beforehand.
+*/
+Item retrieveItem(const Akonadi::Item &item, KAEvent &event)
+{
+    QString mime = CalEvent::mimeType(event.category());
+    event.setItemId(item.id());
+    if (item.hasAttribute<EventAttribute>()) {
+        event.setCommandError(item.attribute<EventAttribute>()->commandError());
+    }
+
+    Item newItem = item;
+    newItem.setMimeType(mime);
+    newItem.setPayload<KAEvent>(event);
+    return newItem;
+}
+
+/******************************************************************************
+* Called when an item has been changed to validate it.
+* Reply = the KAEvent for the item
+*       = invalid if error, in which case errorMsg contains the error message
+*         (which will be empty if the KAEvent is simply invalid).
+*/
+KAEvent checkItemChanged(const Akonadi::Item &item, QString &errorMsg)
+{
+    KAEvent event;
+    if (item.hasPayload<KAEvent>()) {
+        event = item.payload<KAEvent>();
+    }
+    if (event.isValid()) {
+        if (item.remoteId() != event.id()) {
+            qWarning() << "Item ID" << item.remoteId() << "differs from payload ID" << event.id();
+            errorMsg = i18nc("@info", "Item ID %1 differs from payload ID %2.", item.remoteId(), event.id());
+            return KAEvent();
+        }
+    }
+
+    errorMsg.clear();
+    return event;
+}
+
+/******************************************************************************
+* Set a collection's compatibility attribute.
+* Note that because this parameter is set asynchronously by the resource, it
+* can't be stored in the same attribute as other collection parameters which
+* are written by the application. This avoids the resource and application
+* overwriting each other's changes if they attempt simultaneous updates.
+*/
+void setCollectionCompatibility(const Collection &collection, KACalendar::Compat compatibility, int version)
+{
+    qDebug() << collection.id() << "->" << compatibility << version;
+    // Update the CompatibilityAttribute value only.
+    // Note that we can't supply 'collection' to CollectionModifyJob since
+    // that may also contain the CollectionAttribute value, which is read-only
+    // for the resource. So create a new Collection instance and only set a
+    // value for CompatibilityAttribute.
+    Collection col(collection.id());
+    if (!collection.isValid()) {
+        col.setParentCollection(collection.parentCollection());
+        col.setRemoteId(collection.remoteId());
+    }
+    CompatibilityAttribute *attr = col.attribute<CompatibilityAttribute>(Collection::AddIfMissing);
+    attr->setCompatibility(compatibility);
+    attr->setVersion(version);
+    Q_ASSERT(Private::mInstance);
+    CollectionModifyJob *job = new CollectionModifyJob(col, Private::mInstance->parent());
+    Private::mInstance->connect(job, SIGNAL(result(KJob*)), SLOT(modifyCollectionJobDone(KJob*)));
+}
+
+/******************************************************************************
+* Return an error message common to more than one resource.
+*/
+QString errorMessage(ErrorCode code, const QString &param)
+{
+    switch (code) {
+    case UidNotFound:
+        return i18nc("@info", "Event with uid '%1' not found.", param);
+    case NotCurrentFormat:
+        return i18nc("@info", "Calendar is not in current KAlarm format.");
+    case EventNotCurrentFormat:
+        return i18nc("@info", "Event with uid '%1' is not in current KAlarm format.", param);
+    case EventNoAlarms:
+        return i18nc("@info", "Event with uid '%1' contains no usable alarms.", param);
+    case EventReadOnly:
+        return i18nc("@info", "Event with uid '%1' is read only", param);
+    case CalendarAdd:
+        return i18nc("@info", "Failed to add event with uid '%1' to calendar", param);
+    }
+    return QString();
+}
+
+} // namespace KAlarmResourceCommon
+
+/******************************************************************************
+* Called when a collection modification job has completed, to report any error.
+*/
+void Private::modifyCollectionJobDone(KJob *j)
+{
+    qDebug();
+    if (j->error()) {
+        Collection collection = static_cast<CollectionModifyJob *>(j)->collection();
+        qCritical() << "Error: collection id" << collection.id() << ":" << j->errorString();
+    }
+}
+
+#include "kalarmresourcecommon.moc"
+
diff --git a/resources/kalarm/shared/kalarmresourcecommon.h b/resources/kalarm/shared/kalarmresourcecommon.h
new file mode 100644 (file)
index 0000000..9af5170
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ *  kalarmresourcecommon.h  -  common functions for KAlarm Akonadi resources
+ *  Program:  kalarm
+ *  Copyright © 2011-2014 by David Jarvie <djarvie@kde.org>
+ *
+ *  This library is free software; you can redistribute it and/or modify it
+ *  under the terms of the GNU Library General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or (at your
+ *  option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful, but WITHOUT
+ *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ *  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ *  License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public License
+ *  along with this library; see the file COPYING.LIB.  If not, write to the
+ *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ *  02110-1301, USA.
+ */
+
+#ifndef KALARMRESOURCECOMMON_H
+#define KALARMRESOURCECOMMON_H
+
+#include <kalarmcal/kacalendar.h>
+#include <kalarmcal/kaevent.h>
+
+#include <QObject>
+
+namespace KCalCore
+{
+class FileStorage;
+}
+namespace Akonadi
+{
+class Collection;
+class Item;
+}
+using namespace KAlarmCal;
+
+namespace KAlarmResourceCommon
+{
+void          initialise(QObject *parent);
+//    void          customizeConfigDialog(SingleFileResourceConfigDialog<Settings>*);
+KACalendar::Compat getCompatibility(const KCalCore::FileStorage::Ptr &, int &version);
+Akonadi::Item retrieveItem(const Akonadi::Item &, KAEvent &);
+KAEvent       checkItemChanged(const Akonadi::Item &, QString &errorMsg);
+void          setCollectionCompatibility(const Akonadi::Collection &, KACalendar::Compat, int version);
+
+enum ErrorCode {
+    UidNotFound,
+    NotCurrentFormat,
+    EventNotCurrentFormat,
+    EventNoAlarms,
+    EventReadOnly,
+    CalendarAdd
+};
+QString       errorMessage(ErrorCode, const QString &param = QString());
+}
+
+#endif // KALARMRESOURCECOMMON_H
+
diff --git a/resources/kolab/64-apps-kolab.png b/resources/kolab/64-apps-kolab.png
new file mode 100644 (file)
index 0000000..f7edd6c
Binary files /dev/null and b/resources/kolab/64-apps-kolab.png differ
diff --git a/resources/kolab/CMakeLists.txt b/resources/kolab/CMakeLists.txt
new file mode 100644 (file)
index 0000000..52cb089
--- /dev/null
@@ -0,0 +1,70 @@
+include_directories(
+    ${CMAKE_CURRENT_SOURCE_DIR}/../imap
+    ${CMAKE_CURRENT_BINARY_DIR}/../imap
+    ${Libkolabxml_INCLUDES}
+    ${Libkolab_INCLUDES}
+)
+add_definitions( -DQT_NO_CAST_FROM_ASCII )
+add_definitions( -DQT_NO_CAST_TO_ASCII )
+
+
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_imap_resource\")
+
+
+########### next target ###############
+
+set(kolabresource_SRCS
+    ../imap/imapresource.cpp
+    ../imap/settingspasswordrequester.cpp
+    ../imap/setupserver.cpp
+    ../imap/serverinfodialog.cpp
+    kolabretrievecollectionstask.cpp
+    kolabresource.cpp
+    kolabresourcestate.cpp
+    kolabhelpers.cpp
+    kolabmessagehelper.cpp
+    kolabaddtagtask.cpp
+    kolabchangetagtask.cpp
+    kolabremovetagtask.cpp
+    kolabretrievetagstask.cpp
+    kolabsettings.cpp
+    tagchangehelper.cpp
+    kolabrelationresourcetask.cpp
+    kolabchangeitemsrelationstask.cpp
+    kolabchangeitemstagstask.cpp
+    updatemessagejob.cpp
+)
+
+ecm_qt_declare_logging_category(kolabresource_SRCS HEADER kolabresource_debug.h IDENTIFIER KOLABRESOURCE_LOG CATEGORY_NAME log_kolabresource)
+
+kconfig_add_kcfg_files(kolabresource_SRCS ../imap/settingsbase.kcfgc)
+
+ki18n_wrap_ui(kolabresource_SRCS ../imap/setupserverview_desktop.ui)
+ki18n_wrap_ui(kolabresource_SRCS ../imap/serverinfo.ui)
+add_executable(akonadi_kolab_resource ${kolabresource_SRCS})
+
+target_link_libraries(akonadi_kolab_resource
+    KF5::AkonadiCore
+    KF5::IMAP
+    KF5::MailTransport
+    KF5::Mime
+    KF5::AkonadiMime
+    KF5::AkonadiCalendar
+    KF5::AkonadiWidgets
+    KF5::IdentityManagement
+    imapresource
+    folderarchivesettings
+    ${Libkolab_LIBRARIES}
+    KF5::Contacts
+    KF5::CalendarCore
+    akonadi-singlefileresource
+)
+
+install(FILES kolabresource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents")
+install(TARGETS akonadi_kolab_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
+ecm_install_icons(ICONS 64-apps-kolab.png DESTINATION ${KDE_INSTALL_ICONDIR} THEME oxygen)
+
+if (BUILD_TESTING)
+    #add_subdirectory(tests)
+endif()
+add_subdirectory(wizard)
diff --git a/resources/kolab/kolabaddtagtask.cpp b/resources/kolab/kolabaddtagtask.cpp
new file mode 100644 (file)
index 0000000..6a8140f
--- /dev/null
@@ -0,0 +1,194 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Krammer <kevin.krammer@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "kolabaddtagtask.h"
+#include "kolabresource_debug.h"
+#include "../imap/uidnextattribute.h"
+
+#include <kolabobject.h>
+
+#include <kimap/appendjob.h>
+#include <kimap/imapset.h>
+#include <kimap/searchjob.h>
+#include <kimap/selectjob.h>
+#include <kimap/session.h>
+
+#include <KDE/KLocalizedString>
+
+#include <QUuid>
+
+KolabAddTagTask::KolabAddTagTask(ResourceStateInterface::Ptr resource, QObject *parent)
+    : KolabRelationResourceTask(resource, parent)
+{
+}
+
+void KolabAddTagTask::startRelationTask(KIMAP::Session *session)
+{
+    qCDebug(KOLABRESOURCE_LOG) << "converted tag";
+
+    const QLatin1String productId("Akonadi-Kolab-Resource");
+    const KMime::Message::Ptr message = Kolab::KolabObjectWriter::writeTag(resourceState()->tag(), QStringList(), Kolab::KolabV3, productId);
+    mMessageId = message->messageID()->asUnicodeString().toUtf8();
+
+    KIMAP::AppendJob *job = new KIMAP::AppendJob(session);
+    job->setMailBox(mailBoxForCollection(relationCollection()));
+    job->setContent(message->encodedContent(true));
+    job->setInternalDate(message->date()->dateTime());
+    connect(job, &KJob::result, this, &KolabAddTagTask::onAppendMessageDone);
+    job->start();
+}
+
+void KolabAddTagTask::applyFoundUid(qint64 uid)
+{
+    Akonadi::Tag tag = resourceState()->tag();
+
+    //If we failed to get the remoteid the tag remains local only
+    if (uid > 0) {
+        tag.setRemoteId(QByteArray::number(uid));
+    }
+
+    qCDebug(KOLABRESOURCE_LOG) << "comitting new tag";
+    changeCommitted(tag);
+
+    Akonadi::Collection c = relationCollection();
+
+    // Get the current uid next value and store it
+    UidNextAttribute *uidAttr = 0;
+    int oldNextUid = 0;
+    if (c.hasAttribute("uidnext")) {
+        uidAttr = static_cast<UidNextAttribute *>(c.attribute("uidnext"));
+        oldNextUid = uidAttr->uidNext();
+    }
+
+    // If the uid we just got back is the expected next one of the box
+    // then update the property to the probable next uid to keep the cache in sync.
+    // If not something happened in our back, so we don't update and a refetch will
+    // happen at some point.
+    if (uid == oldNextUid) {
+        if (uidAttr == 0) {
+            uidAttr = new UidNextAttribute(uid + 1);
+            c.addAttribute(uidAttr);
+        } else {
+            uidAttr->setUidNext(uid + 1);
+        }
+
+        applyCollectionChanges(c);
+    }
+}
+
+void KolabAddTagTask::triggerSearchJob(KIMAP::Session *session)
+{
+    KIMAP::SearchJob *search = new KIMAP::SearchJob(session);
+
+    search->setUidBased(true);
+    search->setSearchLogic(KIMAP::SearchJob::And);
+
+    if (!mMessageId.isEmpty()) {
+        QByteArray header = "Message-ID ";
+        header += mMessageId;
+
+        search->addSearchCriteria(KIMAP::SearchJob::Header, header);
+    } else {
+        search->addSearchCriteria(KIMAP::SearchJob::New);
+
+        UidNextAttribute *uidNext = relationCollection().attribute<UidNextAttribute>();
+        if (!uidNext) {
+            cancelTask(i18n("Could not determine the UID for the newly created message on the server"));
+            search->deleteLater();
+            return;
+        }
+        KIMAP::ImapInterval interval(uidNext->uidNext());
+
+        search->addSearchCriteria(KIMAP::SearchJob::Uid, interval.toImapSequence());
+    }
+
+    connect(search, &KJob::result,
+            this, &KolabAddTagTask::onSearchDone);
+
+    search->start();
+}
+
+void KolabAddTagTask::onAppendMessageDone(KJob *job)
+
+{
+    KIMAP::AppendJob *append = qobject_cast<KIMAP::AppendJob *>(job);
+
+    if (append->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << append->errorString();
+        cancelTask(append->errorString());
+        return;
+    }
+
+    qint64 uid = append->uid();
+    qCDebug(KOLABRESOURCE_LOG) << "appended message with uid: " << uid;
+
+    if (uid > 0) {
+        // We got it directly if UIDPLUS is supported...
+        applyFoundUid(uid);
+
+    } else {
+        // ... otherwise prepare searching for the message
+        KIMAP::Session *session = append->session();
+        const QString mailBox = append->mailBox();
+
+        if (session->selectedMailBox() != mailBox) {
+            KIMAP::SelectJob *select = new KIMAP::SelectJob(session);
+            select->setMailBox(mailBox);
+
+            connect(select, &KJob::result,
+                    this, &KolabAddTagTask::onPreSearchSelectDone);
+
+            select->start();
+
+        } else {
+            triggerSearchJob(session);
+        }
+    }
+}
+
+void KolabAddTagTask::onPreSearchSelectDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << job->errorString();
+        cancelTask(job->errorString());
+    } else {
+        KIMAP::SelectJob *select = static_cast<KIMAP::SelectJob *>(job);
+        triggerSearchJob(select->session());
+    }
+}
+
+void KolabAddTagTask::onSearchDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << job->errorString();
+        cancelTask(job->errorString());
+        return;
+    }
+
+    KIMAP::SearchJob *search = static_cast<KIMAP::SearchJob *>(job);
+
+    qint64 uid = 0;
+    if (search->results().count() == 1) {
+        uid = search->results().at(0);
+    }
+
+    applyFoundUid(uid);
+}
diff --git a/resources/kolab/kolabaddtagtask.h b/resources/kolab/kolabaddtagtask.h
new file mode 100644 (file)
index 0000000..bb28827
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Krammer <kevin.krammer@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef KOLABADDTAGTASK_H
+#define KOLABADDTAGTASK_H
+
+#include "kolabrelationresourcetask.h"
+
+class KolabAddTagTask : public KolabRelationResourceTask
+{
+    Q_OBJECT
+public:
+    explicit KolabAddTagTask(ResourceStateInterface::Ptr resource, QObject *parent = Q_NULLPTR);
+
+protected:
+    virtual void startRelationTask(KIMAP::Session *session);
+
+private:
+    QByteArray mMessageId;
+
+private:
+    void applyFoundUid(qint64 uid);
+    void triggerSearchJob(KIMAP::Session *session);
+
+private slots:
+    void onAppendMessageDone(KJob *job);
+    void onPreSearchSelectDone(KJob *job);
+    void onSearchDone(KJob *job);
+};
+
+#endif // KOLABADDTAGTASK_H
diff --git a/resources/kolab/kolabchangeitemsrelationstask.cpp b/resources/kolab/kolabchangeitemsrelationstask.cpp
new file mode 100644 (file)
index 0000000..6811c50
--- /dev/null
@@ -0,0 +1,206 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Krammer <kevin.krammer@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "kolabchangeitemsrelationstask.h"
+#include "kolabresource_debug.h"
+#include <imapflags.h>
+#include <kolabobject.h>
+
+#include <kimap/appendjob.h>
+#include <kimap/imapset.h>
+#include <kimap/selectjob.h>
+#include <kimap/session.h>
+#include <kimap/storejob.h>
+
+#include <AkonadiCore/RelationFetchJob>
+#include <AkonadiCore/ItemFetchJob>
+#include <AkonadiCore/ItemFetchScope>
+
+#include "tracer.h"
+#include "kolabhelpers.h"
+
+KolabChangeItemsRelationsTask::KolabChangeItemsRelationsTask(ResourceStateInterface::Ptr resource, QObject *parent)
+    : KolabRelationResourceTask(resource, parent)
+{
+}
+
+void KolabChangeItemsRelationsTask::startRelationTask(KIMAP::Session *session)
+{
+    Trace();
+    mSession = session;
+    mAddedRelations = resourceState()->addedRelations();
+    mRemovedRelations = resourceState()->removedRelations();
+
+    processNextRelation();
+}
+
+void KolabChangeItemsRelationsTask::processNextRelation()
+{
+    Trace() << mAddedRelations.size() << mRemovedRelations.size();
+    Akonadi::Relation relation;
+    if (!mAddedRelations.isEmpty()) {
+        relation = mAddedRelations.takeFirst();
+        mAdding = true;
+    } else if (!mRemovedRelations.isEmpty()) {
+        relation = mRemovedRelations.takeFirst();
+        mAdding = false;
+    } else {
+        Trace() << "Processing done";
+        changeProcessed();
+        return;
+    }
+    Trace() << "Processing " << (mAdding ? " add " : " remove ") << relation;
+
+    //We have to fetch it again in case it changed since the notification was emitted (which is likely)
+    //Otherwise we get an empty remoteid for new tags that were immediately applied on an item
+    Akonadi::RelationFetchJob *fetch = new Akonadi::RelationFetchJob(relation);
+    connect(fetch, &KJob::result, this, &KolabChangeItemsRelationsTask::onRelationFetchDone);
+}
+
+void KolabChangeItemsRelationsTask::onRelationFetchDone(KJob *job)
+{
+    Trace();
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << "RelatonFetch failed: " << job->errorString();
+        processNextRelation();
+        return;
+    }
+
+    const Akonadi::Relation::List relations = static_cast<Akonadi::RelationFetchJob *>(job)->relations();
+    if (relations.size() != 1) {
+        qCWarning(KOLABRESOURCE_LOG) << "Invalid number of relations retrieved: " << relations.size();
+        processNextRelation();
+        return;
+    }
+
+    Akonadi::Relation relation = relations.first();
+    if (mAdding) {
+        addRelation(relation);
+    } else {
+        removeRelation(relation);
+    }
+}
+
+void KolabChangeItemsRelationsTask::addRelation(const Akonadi::Relation &relation)
+{
+    Akonadi::ItemFetchJob *fetchJob = new Akonadi::ItemFetchJob(Akonadi::Item::List() << relation.left() << relation.right());
+    fetchJob->fetchScope().setCacheOnly(true);
+    fetchJob->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::All);
+    fetchJob->fetchScope().setFetchGid(true);
+    fetchJob->fetchScope().fetchFullPayload(true);
+    fetchJob->setProperty("relation", QVariant::fromValue(relation));
+    connect(fetchJob, &KJob::result, this, &KolabChangeItemsRelationsTask::onItemsFetched);
+}
+
+void KolabChangeItemsRelationsTask::onItemsFetched(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << "Failed to fetch items for relation: " << job->errorString();
+        processNextRelation();
+        return;
+    }
+    Akonadi::ItemFetchJob *fetchJob = static_cast<Akonadi::ItemFetchJob *>(job);
+    if (fetchJob->items().size() != 2) {
+        qCWarning(KOLABRESOURCE_LOG) << "Invalid number of items retrieved: " << fetchJob->items().size();
+        processNextRelation();
+        return;
+    }
+
+    const Akonadi::Item::List items = fetchJob->items();
+    const Akonadi::Relation relation = job->property("relation").value<Akonadi::Relation>();
+    Akonadi::Item leftItem = items[0] == relation.left() ? items[0] : items[1];
+    Akonadi::Item rightItem = items[0] == relation.right() ? items[0] : items[1];
+    const QString left = KolabHelpers::createMemberUrl(leftItem, resourceState()->userName());
+    const QString right =  KolabHelpers::createMemberUrl(rightItem, resourceState()->userName());
+    if (left.isEmpty() || right.isEmpty()) {
+        qCWarning(KOLABRESOURCE_LOG) << "Failed to add relation, invalid member: " << left << " : " << right;
+        processNextRelation();
+        return;
+    }
+    QStringList members;
+    members.reserve(2);
+    members << left << right;
+
+    const QLatin1String productId("Akonadi-Kolab-Resource");
+    const KMime::Message::Ptr message = Kolab::KolabObjectWriter::writeRelation(relation, members, Kolab::KolabV3, productId);
+
+    KIMAP::AppendJob *appendJob = new KIMAP::AppendJob(mSession);
+    appendJob->setMailBox(mailBoxForCollection(relationCollection()));
+    appendJob->setContent(message->encodedContent(true));
+    appendJob->setInternalDate(message->date()->dateTime());
+    connect(appendJob, &KJob::result, this, &KolabChangeItemsRelationsTask::onChangeCommitted);
+    appendJob->start();
+}
+
+void KolabChangeItemsRelationsTask::removeRelation(const Akonadi::Relation &relation)
+{
+    Trace();
+    mCurrentRelation = relation;
+    const QString mailBox = mailBoxForCollection(relationCollection());
+
+    if (mSession->selectedMailBox() != mailBox) {
+        KIMAP::SelectJob *select = new KIMAP::SelectJob(mSession);
+        select->setMailBox(mailBox);
+
+        connect(select, &KJob::result, this, &KolabChangeItemsRelationsTask::onSelectDone);
+
+        select->start();
+    } else {
+        triggerStoreJob();
+    }
+}
+
+void KolabChangeItemsRelationsTask::onSelectDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << "Failed to select mailbox: " << job->errorString();
+        cancelTask(job->errorString());
+    } else {
+        triggerStoreJob();
+    }
+}
+
+void KolabChangeItemsRelationsTask::triggerStoreJob()
+{
+    KIMAP::ImapSet set;
+    set.add(mCurrentRelation.remoteId().toLong());
+
+    Trace() << set.toImapSequenceSet();
+
+    KIMAP::StoreJob *store = new KIMAP::StoreJob(mSession);
+    store->setUidBased(true);
+    store->setSequenceSet(set);
+    store->setFlags(QList<QByteArray>() << ImapFlags::Deleted);
+    store->setMode(KIMAP::StoreJob::AppendFlags);
+    connect(store, &KJob::result, this, &KolabChangeItemsRelationsTask::onChangeCommitted);
+    store->start();
+}
+
+void KolabChangeItemsRelationsTask::onChangeCommitted(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << "Error while storing change";
+        cancelTask(job->errorString());
+    } else {
+        processNextRelation();
+    }
+}
+
diff --git a/resources/kolab/kolabchangeitemsrelationstask.h b/resources/kolab/kolabchangeitemsrelationstask.h
new file mode 100644 (file)
index 0000000..32083a1
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Krammer <kevin.krammer@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef KOLABCHANGEITEMSRELATIONSTASK_H
+#define KOLABCHANGEITEMSRELATIONSTASK_H
+
+#include "kolabrelationresourcetask.h"
+
+class KolabChangeItemsRelationsTask : public KolabRelationResourceTask
+{
+    Q_OBJECT
+public:
+    explicit KolabChangeItemsRelationsTask(ResourceStateInterface::Ptr resource, QObject *parent = Q_NULLPTR);
+
+protected:
+    virtual void startRelationTask(KIMAP::Session *session);
+
+private Q_SLOTS:
+    void onRelationFetchDone(KJob *job);
+
+    void addRelation(const Akonadi::Relation &relation);
+    void onItemsFetched(KJob *job);
+    void removeRelation(const Akonadi::Relation &relation);
+    void onSelectDone(KJob *job);
+    void triggerStoreJob();
+    void onChangeCommitted(KJob *job);
+
+private:
+    void processNextRelation();
+
+    KIMAP::Session *mSession;
+    Akonadi::Relation::List mAddedRelations;
+    Akonadi::Relation::List mRemovedRelations;
+    Akonadi::Relation mCurrentRelation;
+    bool mAdding;
+};
+
+#endif
diff --git a/resources/kolab/kolabchangeitemstagstask.cpp b/resources/kolab/kolabchangeitemstagstask.cpp
new file mode 100644 (file)
index 0000000..037a3fb
--- /dev/null
@@ -0,0 +1,141 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Krammer <kevin.krammer@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "kolabchangeitemstagstask.h"
+#include "kolabresource_debug.h"
+#include "tagchangehelper.h"
+
+#include <AkonadiCore/ItemFetchJob>
+#include <AkonadiCore/ItemFetchScope>
+#include <AkonadiCore/TagFetchJob>
+
+KolabChangeItemsTagsTask::KolabChangeItemsTagsTask(ResourceStateInterface::Ptr resource, QSharedPointer<TagConverter> tagConverter, QObject *parent)
+    : KolabRelationResourceTask(resource, parent)
+    , mTagConverter(tagConverter)
+{
+}
+
+void KolabChangeItemsTagsTask::startRelationTask(KIMAP::Session *session)
+{
+    mSession = session;
+
+    //It's entierly possible that we don't have an rid yet
+
+    // compile a set of changed tags
+    Q_FOREACH (const Akonadi::Tag &tag, resourceState()->addedTags()) {
+        mChangedTags.append(tag);
+    }
+    Q_FOREACH (const Akonadi::Tag &tag, resourceState()->removedTags()) {
+        mChangedTags.append(tag);
+    }
+    qCDebug(KOLABRESOURCE_LOG) << mChangedTags;
+
+    processNextTag();
+}
+
+void KolabChangeItemsTagsTask::processNextTag()
+{
+    if (mChangedTags.isEmpty()) {
+        changeProcessed();
+        return;
+    }
+
+    // "take first"
+    const Akonadi::Tag tag = mChangedTags.takeFirst();
+
+    //We have to fetch it again in case it changed since the notification was emitted (which is likely)
+    //Otherwise we get an empty remoteid for new tags that were immediately applied on an item
+    Akonadi::TagFetchJob *fetch = new Akonadi::TagFetchJob(tag);
+    connect(fetch, &KJob::result, this, &KolabChangeItemsTagsTask::onTagFetchDone);
+}
+
+void KolabChangeItemsTagsTask::onTagFetchDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << "TagFetch failed: " << job->errorString();
+        // TODO: we could continue for the other tags?
+        cancelTask(job->errorString());
+        return;
+    }
+
+    const Akonadi::Tag::List tags = static_cast<Akonadi::TagFetchJob *>(job)->tags();
+    if (tags.size() != 1) {
+        qCWarning(KOLABRESOURCE_LOG) << "Invalid number of tags retrieved: " << tags.size();
+        // TODO: we could continue for the other tags?
+        cancelTask(job->errorString());
+        return;
+    }
+
+    Akonadi::ItemFetchJob *fetch = new Akonadi::ItemFetchJob(tags.first());
+    fetch->fetchScope().setCacheOnly(true);
+    fetch->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::All);
+    fetch->fetchScope().setFetchGid(true);
+    fetch->fetchScope().fetchFullPayload(true);
+    fetch->setProperty("tag", QVariant::fromValue(tags.first()));
+    connect(fetch, &KJob::result, this, &KolabChangeItemsTagsTask::onItemsFetchDone);
+}
+
+void KolabChangeItemsTagsTask::onItemsFetchDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << "ItemFetch failed: " << job->errorString();
+        // TODO: we could continue for the other tags?
+        cancelTask(job->errorString());
+        return;
+    }
+
+    const Akonadi::Item::List items = static_cast<Akonadi::ItemFetchJob *>(job)->items();
+    qCDebug(KOLABRESOURCE_LOG) << items.size();
+
+    TagChangeHelper *changeHelper = new TagChangeHelper(this);
+
+    connect(changeHelper, &TagChangeHelper::applyCollectionChanges,
+            this, &KolabChangeItemsTagsTask::onApplyCollectionChanged);
+    connect(changeHelper, &TagChangeHelper::cancelTask, this, &KolabChangeItemsTagsTask::onCancelTask);
+    connect(changeHelper, &TagChangeHelper::changeCommitted, this, &KolabChangeItemsTagsTask::onChangeCommitted);
+
+    const Akonadi::Tag tag = job->property("tag").value<Akonadi::Tag>();
+    {
+        qCDebug(KOLABRESOURCE_LOG) << "Writing " << tag.name() << " with " << items.size() << " members to the server: ";
+        foreach (const Akonadi::Item &item, items) {
+            qCDebug(KOLABRESOURCE_LOG) << "member(localid, remoteid): " << item.id() << item.remoteId();
+        }
+    }
+    Q_ASSERT(tag.isValid());
+    changeHelper->start(tag, mTagConverter->createMessage(tag, items, resourceState()->userName()), mSession);
+}
+
+void KolabChangeItemsTagsTask::onApplyCollectionChanged(const Akonadi::Collection &collection)
+{
+    mRelationCollection = collection;
+    applyCollectionChanges(collection);
+}
+
+void KolabChangeItemsTagsTask::onCancelTask(const QString &errorText)
+{
+    // TODO: we could continue for the other tags?
+    cancelTask(errorText);
+}
+
+void KolabChangeItemsTagsTask::onChangeCommitted()
+{
+    processNextTag();
+}
diff --git a/resources/kolab/kolabchangeitemstagstask.h b/resources/kolab/kolabchangeitemstagstask.h
new file mode 100644 (file)
index 0000000..04331d2
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Krammer <kevin.krammer@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef KOLABCHANGEITEMSTAGSTASK_H
+#define KOLABCHANGEITEMSTAGSTASK_H
+
+#include "kolabrelationresourcetask.h"
+#include "tagchangehelper.h"
+
+class KolabChangeItemsTagsTask : public KolabRelationResourceTask
+{
+    Q_OBJECT
+public:
+    explicit KolabChangeItemsTagsTask(ResourceStateInterface::Ptr resource, QSharedPointer<TagConverter> tagConverter, QObject *parent = Q_NULLPTR);
+
+protected:
+    virtual void startRelationTask(KIMAP::Session *session);
+
+private:
+    KIMAP::Session *mSession;
+    QList<Akonadi::Tag> mChangedTags;
+    QSharedPointer<TagConverter> mTagConverter;
+
+private:
+    void processNextTag();
+
+private Q_SLOTS:
+    void onItemsFetchDone(KJob *job);
+    void onTagFetchDone(KJob *job);
+
+    void onApplyCollectionChanged(const Akonadi::Collection &collection);
+    void onCancelTask(const QString &errorText);
+    void onChangeCommitted();
+};
+
+#endif // KOLABCHANGEITEMSTAGSTASK_H
diff --git a/resources/kolab/kolabchangetagtask.cpp b/resources/kolab/kolabchangetagtask.cpp
new file mode 100644 (file)
index 0000000..dfc9c57
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Krammer <kevin.krammer@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "kolabchangetagtask.h"
+#include "kolabresource_debug.h"
+#include "tagchangehelper.h"
+
+#include <AkonadiCore/ItemFetchJob>
+#include <AkonadiCore/ItemFetchScope>
+
+KolabChangeTagTask::KolabChangeTagTask(ResourceStateInterface::Ptr resource, QSharedPointer<TagConverter> tagConverter, QObject *parent)
+    : KolabRelationResourceTask(resource, parent)
+    , mSession(0)
+    , mTagConverter(tagConverter)
+{
+}
+
+void KolabChangeTagTask::startRelationTask(KIMAP::Session *session)
+{
+    mSession = session;
+
+    Akonadi::ItemFetchJob *fetch = new Akonadi::ItemFetchJob(resourceState()->tag());
+    fetch->fetchScope().setCacheOnly(true);
+    fetch->fetchScope().setFetchGid(true);
+    fetch->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::All);
+    fetch->fetchScope().fetchFullPayload(true);
+    connect(fetch, &KJob::result, this, &KolabChangeTagTask::onItemsFetchDone);
+}
+
+void KolabChangeTagTask::onItemsFetchDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << "ItemFetch failed: " << job->errorString();
+        cancelTask(job->errorString());
+        return;
+    }
+
+    const Akonadi::Item::List items = static_cast<Akonadi::ItemFetchJob *>(job)->items();
+
+    TagChangeHelper *changeHelper = new TagChangeHelper(this);
+
+    connect(changeHelper, &TagChangeHelper::applyCollectionChanges,
+            this, &KolabChangeTagTask::onApplyCollectionChanged);
+    connect(changeHelper, &TagChangeHelper::cancelTask, this, &KolabChangeTagTask::onCancelTask);
+    connect(changeHelper, &TagChangeHelper::changeCommitted, this, &KolabChangeTagTask::onChangeCommitted);
+
+    changeHelper->start(resourceState()->tag(), mTagConverter->createMessage(resourceState()->tag(), items, resourceState()->userName()), mSession);
+}
+
+void KolabChangeTagTask::onApplyCollectionChanged(const Akonadi::Collection &collection)
+{
+    mRelationCollection = collection;
+    applyCollectionChanges(collection);
+}
+
+void KolabChangeTagTask::onCancelTask(const QString &errorText)
+{
+    cancelTask(errorText);
+}
+
+void KolabChangeTagTask::onChangeCommitted()
+{
+    changeProcessed();
+}
diff --git a/resources/kolab/kolabchangetagtask.h b/resources/kolab/kolabchangetagtask.h
new file mode 100644 (file)
index 0000000..961f247
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Krammer <kevin.krammer@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef KOLABCHANGETAGTASK_H
+#define KOLABCHANGETAGTASK_H
+
+#include "kolabrelationresourcetask.h"
+#include "tagchangehelper.h"
+
+class KolabChangeTagTask : public KolabRelationResourceTask
+{
+    Q_OBJECT
+public:
+    explicit KolabChangeTagTask(ResourceStateInterface::Ptr resource, QSharedPointer<TagConverter> tagConverter, QObject *parent = Q_NULLPTR);
+
+protected:
+    virtual void startRelationTask(KIMAP::Session *session);
+
+private:
+    KIMAP::Session *mSession;
+    QSharedPointer<TagConverter> mTagConverter;
+
+private Q_SLOTS:
+    void onItemsFetchDone(KJob *job);
+
+    void onApplyCollectionChanged(const Akonadi::Collection &collection);
+    void onCancelTask(const QString &errorText);
+    void onChangeCommitted();
+};
+
+#endif // KOLABCHANGETAGTASK_H
diff --git a/resources/kolab/kolabhelpers.cpp b/resources/kolab/kolabhelpers.cpp
new file mode 100644 (file)
index 0000000..df913bb
--- /dev/null
@@ -0,0 +1,502 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "kolabhelpers.h"
+#include "kolabresource_debug.h"
+#include <KMime/KMimeMessage>
+#include <KCalCore/Incidence>
+#include <AkonadiCore/Collection>
+#include <AkonadiCore/ItemFetchJob>
+#include <AkonadiCore/ItemFetchScope>
+#include <akonadi/notes/noteutils.h>
+#include <kolabobject.h>
+#include <errorhandler.h>
+#include <KLocalizedString>
+#include "tracer.h"
+
+bool KolabHelpers::checkForErrors(const Akonadi::Item &item)
+{
+    if (!Kolab::ErrorHandler::instance().errorOccured()) {
+        Kolab::ErrorHandler::instance().clear();
+        return false;
+    }
+
+    QString errorMsg;
+    foreach (const Kolab::ErrorHandler::Err &error, Kolab::ErrorHandler::instance().getErrors()) {
+        errorMsg.append(error.message);
+        errorMsg.append(QLatin1String("\n"));
+    }
+
+    qCWarning(KOLABRESOURCE_LOG) << "Error on item with id: " << item.id() << " remote id: " << item.remoteId() << ":\n" << errorMsg;
+    Kolab::ErrorHandler::instance().clear();
+    return true;
+}
+
+Akonadi::Item getErrorItem(Kolab::FolderType folderType, const QString &remoteId)
+{
+    //TODO set title, text and icon
+    Akonadi::Item item;
+    item.setRemoteId(remoteId);
+    switch (folderType) {
+    case Kolab::EventType: {
+        KCalCore::Event::Ptr event(new KCalCore::Event);
+        //FIXME Use message creation date time
+        event->setDtStart(KDateTime::currentUtcDateTime());
+        event->setSummary(i18n("Corrupt Event"));
+        event->setDescription(i18n("Event could not be read. Delete this event to remove it from the server."));
+        item.setMimeType(KCalCore::Event::eventMimeType());
+        item.setPayload(event);
+    }
+    break;
+    case Kolab::TaskType: {
+        KCalCore::Todo::Ptr task(new KCalCore::Todo);
+        //FIXME Use message creation date time
+        task->setDtStart(KDateTime::currentUtcDateTime());
+        task->setSummary(i18n("Corrupt Task"));
+        task->setDescription(i18n("Task could not be read. Delete this task to remove it from the server."));
+        item.setMimeType(KCalCore::Todo::todoMimeType());
+        item.setPayload(task);
+    }
+    break;
+    case Kolab::JournalType: {
+        KCalCore::Journal::Ptr journal(new KCalCore::Journal);
+        //FIXME Use message creation date time
+        journal->setDtStart(KDateTime::currentUtcDateTime());
+        journal->setSummary(i18n("Corrupt journal"));
+        journal->setDescription(i18n("Journal could not be read. Delete this journal to remove it from the server."));
+        item.setMimeType(KCalCore::Journal::journalMimeType());
+        item.setPayload(journal);
+    }
+    break;
+    case Kolab::ContactType: {
+        KContacts::Addressee addressee;
+        addressee.setName(i18n("Corrupt Contact"));
+        addressee.setNote(i18n("Contact could not be read. Delete this contact to remove it from the server."));
+        item.setMimeType(KContacts::Addressee::mimeType());
+        item.setPayload(addressee);
+    }
+    break;
+    case Kolab::NoteType: {
+        Akonadi::NoteUtils::NoteMessageWrapper note;
+        note.setTitle(i18n("Corrupt Note"));
+        note.setText(i18n("Note could not be read. Delete this note to remove it from the server."));
+        item.setPayload(Akonadi::NoteUtils::noteMimeType());
+        item.setPayload(note.message());
+    }
+    break;
+    case Kolab::MailType:
+    //We don't convert mails, so that should never fail.
+    default:
+        qCWarning(KOLABRESOURCE_LOG) << "unhandled folder type: " << folderType;
+    }
+    return item;
+}
+
+Akonadi::Item KolabHelpers::translateFromImap(Kolab::FolderType folderType, const Akonadi::Item &imapItem, bool &ok)
+{
+    //Avoid trying to convert imap messages
+    if (folderType == Kolab::MailType) {
+        return imapItem;
+    }
+
+    //No payload, so it's a flag change. We ignore flag changes on groupware data.
+    if (!imapItem.hasPayload()) {
+        ok = false;
+        return Akonadi::Item();
+    }
+    if (!imapItem.hasPayload<KMime::Message::Ptr>()) {
+        qCWarning(KOLABRESOURCE_LOG) << "Payload is not a MessagePtr!";
+        Q_ASSERT(false);
+        ok = false;
+        return Akonadi::Item();
+    }
+
+    const KMime::Message::Ptr payload = imapItem.payload<KMime::Message::Ptr>();
+    const Kolab::KolabObjectReader reader(payload);
+    if (checkForErrors(imapItem)) {
+        ok = true;
+        //We return an error object so the sync keeps working, and we can clean up the mess by simply deleting the object in the application.
+        return getErrorItem(folderType, imapItem.remoteId());
+    }
+    switch (reader.getType()) {
+    case Kolab::EventObject:
+    case Kolab::TodoObject:
+    case Kolab::JournalObject: {
+        const KCalCore::Incidence::Ptr incidencePtr = reader.getIncidence();
+        if (!incidencePtr) {
+            qCWarning(KOLABRESOURCE_LOG) << "Failed to read incidence.";
+            ok = false;
+            return Akonadi::Item();
+        }
+        Akonadi::Item newItem(incidencePtr->mimeType());
+        newItem.setPayload(incidencePtr);
+        newItem.setRemoteId(imapItem.remoteId());
+        newItem.setGid(incidencePtr->instanceIdentifier());
+        return newItem;
+    }
+    break;
+    case Kolab::NoteObject: {
+        const KMime::Message::Ptr note = reader.getNote();
+        if (!note) {
+            qCWarning(KOLABRESOURCE_LOG) << "Failed to read note.";
+            ok = false;
+            return Akonadi::Item();
+        }
+        Akonadi::Item newItem(QStringLiteral("text/x-vnd.akonadi.note"));
+        newItem.setPayload(note);
+        newItem.setRemoteId(imapItem.remoteId());
+        const Akonadi::NoteUtils::NoteMessageWrapper wrapper(note);
+        newItem.setGid(wrapper.uid());
+        return newItem;
+    }
+    break;
+    case Kolab::ContactObject: {
+        Akonadi::Item newItem(KContacts::Addressee::mimeType());
+        newItem.setPayload(reader.getContact());
+        newItem.setRemoteId(imapItem.remoteId());
+        newItem.setGid(reader.getContact().uid());
+        return newItem;
+    }
+    break;
+    case Kolab::DistlistObject: {
+        KContacts::ContactGroup contactGroup = reader.getDistlist();
+
+        QList<KContacts::ContactGroup::ContactReference> toAdd;
+        for (uint index = 0; index < contactGroup.contactReferenceCount(); ++index) {
+            const KContacts::ContactGroup::ContactReference &reference = contactGroup.contactReference(index);
+            KContacts::ContactGroup::ContactReference ref;
+            ref.setGid(reference.uid()); //libkolab set a gid with setUid()
+            toAdd << ref;
+        }
+        contactGroup.removeAllContactReferences();
+        foreach (const KContacts::ContactGroup::ContactReference &ref, toAdd) {
+            contactGroup.append(ref);
+        }
+
+        Akonadi::Item newItem(KContacts::ContactGroup::mimeType());
+        newItem.setPayload(contactGroup);
+        newItem.setRemoteId(imapItem.remoteId());
+        newItem.setGid(contactGroup.id());
+        return newItem;
+    }
+    break;
+    default:
+        qCWarning(KOLABRESOURCE_LOG) << "Object type not handled";
+        ok = false;
+        break;
+    }
+    return Akonadi::Item();
+}
+
+Akonadi::Item::List KolabHelpers::translateToImap(const Akonadi::Item::List &items, bool &ok)
+{
+    Akonadi::Item::List imapItems;
+    Q_FOREACH (const Akonadi::Item &item, items) {
+        bool translationOk = true;
+        imapItems << translateToImap(item, translationOk);
+        if (!translationOk) {
+            ok = false;
+        }
+    }
+    return imapItems;
+}
+
+static KContacts::ContactGroup convertToGidOnly(const KContacts::ContactGroup &contactGroup)
+{
+    QList<KContacts::ContactGroup::ContactReference> toAdd;
+    for (uint index = 0; index < contactGroup.contactReferenceCount(); ++index) {
+        const KContacts::ContactGroup::ContactReference &reference = contactGroup.contactReference(index);
+        QString gid;
+        if (!reference.gid().isEmpty()) {
+            gid = reference.gid();
+        } else {
+            // WARNING: this is an ugly hack for backwards compatibility. Normally this codepath shouldn't be hit.
+            // Replace all references with real data-sets
+            // Hopefully all resources are available during saving, so we can look up
+            // in the addressbook to get name+email from the UID.
+
+            const Akonadi::Item item(reference.uid().toLongLong());
+            Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob(item);
+            job->fetchScope().fetchFullPayload();
+            if (!job->exec()) {
+                continue;
+            }
+
+            const Akonadi::Item::List items = job->items();
+            if (items.count() != 1) {
+                continue;
+            }
+            const KContacts::Addressee addressee = job->items().at(0).payload<KContacts::Addressee>();
+            gid = addressee.uid();
+        }
+        KContacts::ContactGroup::ContactReference ref;
+        ref.setUid(gid); //libkolab expects a gid for uid()
+        toAdd << ref;
+    }
+    KContacts::ContactGroup gidOnlyContactGroup = contactGroup;
+    gidOnlyContactGroup.removeAllContactReferences();
+    foreach (const KContacts::ContactGroup::ContactReference &ref, toAdd) {
+        gidOnlyContactGroup.append(ref);
+    }
+    return gidOnlyContactGroup;
+}
+
+Akonadi::Item KolabHelpers::translateToImap(const Akonadi::Item &item, bool &ok)
+{
+    ok = true;
+    //imap messages don't need to be translated
+    if (item.mimeType() == KMime::Message::mimeType()) {
+        Q_ASSERT(item.hasPayload<KMime::Message::Ptr>());
+        return item;
+    }
+    const QLatin1String productId("Akonadi-Kolab-Resource");
+    //Everthing stays the same, except mime type and payload
+    Akonadi::Item imapItem = item;
+    imapItem.setMimeType(QStringLiteral("message/rfc822"));
+    try {
+        switch (getKolabTypeFromMimeType(item.mimeType())) {
+        case Kolab::EventObject:
+        case Kolab::TodoObject:
+        case Kolab::JournalObject: {
+            qCDebug(KOLABRESOURCE_LOG) << "converted event";
+            const KMime::Message::Ptr message = Kolab::KolabObjectWriter::writeIncidence(
+                                                    item.payload<KCalCore::Incidence::Ptr>(),
+                                                    Kolab::KolabV3, productId, QStringLiteral("UTC"));
+            imapItem.setPayload(message);
+        }
+        break;
+        case Kolab::NoteObject: {
+            qCDebug(KOLABRESOURCE_LOG) << "converted note";
+            const KMime::Message::Ptr message = Kolab::KolabObjectWriter::writeNote(
+                                                    item.payload<KMime::Message::Ptr>(), Kolab::KolabV3, productId);
+            imapItem.setPayload(message);
+        }
+        break;
+        case Kolab::ContactObject: {
+            qCDebug(KOLABRESOURCE_LOG) << "converted contact";
+            const KMime::Message::Ptr message = Kolab::KolabObjectWriter::writeContact(
+                                                    item.payload<KContacts::Addressee>(), Kolab::KolabV3, productId);
+            imapItem.setPayload(message);
+        }
+        break;
+        case Kolab::DistlistObject: {
+            const KContacts::ContactGroup contactGroup = convertToGidOnly(item.payload<KContacts::ContactGroup>());
+            qCDebug(KOLABRESOURCE_LOG) << "converted distlist";
+            const KMime::Message::Ptr message = Kolab::KolabObjectWriter::writeDistlist(
+                                                    contactGroup, Kolab::KolabV3, productId);
+            imapItem.setPayload(message);
+        }
+        break;
+        default:
+            qCWarning(KOLABRESOURCE_LOG) << "object type not handled: " << item.id() << item.mimeType();
+            ok = false;
+            return Akonadi::Item();
+
+        }
+    } catch (const Akonadi::PayloadException &e) {
+        qCWarning(KOLABRESOURCE_LOG) << "The item contains the wrong or no payload: " << item.id() << item.mimeType();
+        qCWarning(KOLABRESOURCE_LOG) << e.what();
+        return Akonadi::Item();
+    }
+
+    if (checkForErrors(item)) {
+        qCWarning(KOLABRESOURCE_LOG) << "an error occurred while trying to translate the item to the kolab format: " << item.id();
+        ok = false;
+        return Akonadi::Item();
+    }
+    return imapItem;
+}
+
+QByteArray KolabHelpers::kolabTypeForMimeType(const QStringList &contentMimeTypes)
+{
+    if (contentMimeTypes.contains(KContacts::Addressee::mimeType())) {
+        return "contact";
+    } else if (contentMimeTypes.contains(KCalCore::Event::eventMimeType())) {
+        return "event";
+    } else if (contentMimeTypes.contains(KCalCore::Todo::todoMimeType())) {
+        return "task";
+    } else if (contentMimeTypes.contains(KCalCore::Journal::journalMimeType())) {
+        return "journal";
+    } else if (contentMimeTypes.contains(QStringLiteral("application/x-vnd.akonadi.note")) ||
+               contentMimeTypes.contains(QStringLiteral("text/x-vnd.akonadi.note"))) {
+        return "note";
+    }
+    return QByteArray();
+}
+
+Kolab::ObjectType KolabHelpers::getKolabTypeFromMimeType(const QString &type)
+{
+    if (type == KCalCore::Event::eventMimeType()) {
+        return Kolab::EventObject;
+    } else if (type == KCalCore::Todo::todoMimeType()) {
+        return Kolab::TodoObject;
+    } else if (type == KCalCore::Journal::journalMimeType()) {
+        return Kolab::JournalObject;
+    } else if (type == KContacts::Addressee::mimeType()) {
+        return Kolab::ContactObject;
+    } else if (type == KContacts::ContactGroup::mimeType()) {
+        return Kolab::DistlistObject;
+    } else if (type == QLatin1String("text/x-vnd.akonadi.note") ||
+               type == QLatin1String("application/x-vnd.akonadi.note")) {
+        return Kolab::NoteObject;
+    }
+    return Kolab::InvalidObject;
+}
+
+QString KolabHelpers::getMimeType(Kolab::FolderType type)
+{
+    switch (type) {
+    case Kolab::MailType:
+        return KMime::Message::mimeType();
+    case Kolab::ConfigurationType:
+        return QStringLiteral(KOLAB_TYPE_RELATION);
+    default:
+        qCDebug(KOLABRESOURCE_LOG) << "unhandled folder type: " << type;
+    }
+    return QString();
+}
+
+QStringList KolabHelpers::getContentMimeTypes(Kolab::FolderType type)
+{
+    QStringList contentTypes;
+    contentTypes << Akonadi::Collection::mimeType();
+    switch (type) {
+    case Kolab::EventType:
+        contentTypes <<  KCalCore::Event().mimeType();
+        break;
+    case Kolab::TaskType:
+        contentTypes <<  KCalCore::Todo().mimeType();
+        break;
+    case Kolab::JournalType:
+        contentTypes <<  KCalCore::Journal().mimeType();
+        break;
+    case Kolab::ContactType:
+        contentTypes << KContacts::Addressee::mimeType() << KContacts::ContactGroup::mimeType();
+        break;
+    case Kolab::NoteType:
+        contentTypes << QStringLiteral("text/x-vnd.akonadi.note") << QStringLiteral("application/x-vnd.akonadi.note");
+        break;
+    case Kolab::MailType:
+        contentTypes << KMime::Message::mimeType();
+        break;
+    case Kolab::ConfigurationType:
+        contentTypes << QStringLiteral(KOLAB_TYPE_RELATION);
+        break;
+    default:
+        break;
+    }
+    return contentTypes;
+}
+
+Kolab::FolderType KolabHelpers::folderTypeFromString(const QByteArray &folderTypeName)
+{
+    return Kolab::folderTypeFromString(std::string(folderTypeName.data(), folderTypeName.size()));
+}
+
+QByteArray KolabHelpers::getFolderTypeAnnotation(const QMap< QByteArray, QByteArray > &annotations)
+{
+    if (annotations.contains("/shared" KOLAB_FOLDER_TYPE_ANNOTATION)) {
+        return annotations.value("/shared" KOLAB_FOLDER_TYPE_ANNOTATION);
+    }
+    return annotations.value(KOLAB_FOLDER_TYPE_ANNOTATION);
+}
+
+void KolabHelpers::setFolderTypeAnnotation(QMap< QByteArray, QByteArray > &annotations, const QByteArray &value)
+{
+    annotations["/shared" KOLAB_FOLDER_TYPE_ANNOTATION] = value;
+}
+
+QString KolabHelpers::getIcon(Kolab::FolderType type)
+{
+    switch (type) {
+    case Kolab::EventType:
+    case Kolab::TaskType:
+    case Kolab::JournalType:
+        return QStringLiteral("view-calendar");
+    case Kolab::ContactType:
+        return QStringLiteral("view-pim-contacts");
+    case Kolab::NoteType:
+        return QStringLiteral("view-pim-notes");
+    case Kolab::MailType:
+    case Kolab::ConfigurationType:
+    case Kolab::FreebusyType:
+    case Kolab::FileType:
+    default:
+        break;
+    }
+    return QString();
+}
+
+bool KolabHelpers::isHandledType(Kolab::FolderType type)
+{
+    switch (type) {
+    case Kolab::EventType:
+    case Kolab::TaskType:
+    case Kolab::JournalType:
+    case Kolab::ContactType:
+    case Kolab::NoteType:
+    case Kolab::MailType:
+        return true;
+    case Kolab::ConfigurationType:
+    case Kolab::FreebusyType:
+    case Kolab::FileType:
+    default:
+        break;
+    }
+    return false;
+}
+
+QList<QByteArray> KolabHelpers::ancestorChain(const Akonadi::Collection &col)
+{
+    Q_ASSERT(col.isValid());
+    if (col.parentCollection() == Akonadi::Collection::root() || col == Akonadi::Collection::root() || !col.isValid()) {
+        return QList<QByteArray>();
+    }
+    QList<QByteArray> ancestors = ancestorChain(col.parentCollection());
+    Q_ASSERT(!col.remoteId().isEmpty());
+    ancestors << col.remoteId().toLatin1().mid(1); //We strip the first character which is always the separator
+    return ancestors;
+}
+
+QString KolabHelpers::createMemberUrl(const Akonadi::Item &item, const QString &user)
+{
+    Trace() << item.id() << item.mimeType() << item.gid() << item.hasPayload();
+    Kolab::RelationMember member;
+    if (item.mimeType() == KMime::Message::mimeType()) {
+        if (!item.hasPayload<KMime::Message::Ptr>()) {
+            qCWarning(KOLABRESOURCE_LOG) << "Email without payload, failed to add to tag: " << item.id() << item.remoteId();
+            return QString();
+        }
+        KMime::Message::Ptr msg = item.payload<KMime::Message::Ptr>();
+        member.uid = item.remoteId().toLong();
+        member.user = user;
+        member.subject = msg->subject()->asUnicodeString();
+        member.messageId = msg->messageID()->asUnicodeString();
+        member.date = msg->date()->asUnicodeString();
+        member.mailbox = ancestorChain(item.parentCollection());
+    } else {
+        if (item.gid().isEmpty()) {
+            qCWarning(KOLABRESOURCE_LOG) << "Groupware object without GID, failed to add to tag: " << item.id() << item.remoteId();
+            return QString();
+        }
+        member.gid = item.gid();
+    }
+    return Kolab::generateMemberUrl(member);
+}
+
diff --git a/resources/kolab/kolabhelpers.h b/resources/kolab/kolabhelpers.h
new file mode 100644 (file)
index 0000000..21805cf
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef KOLABHELPERS_H
+#define KOLABHELPERS_H
+
+#include <AkonadiCore/Item>
+#include <kolabdefinitions.h> //libkolab
+#include <formathelpers.h> //libkolab
+
+class KolabHelpers
+{
+public:
+    static bool checkForErrors(const Akonadi::Item &affectedItem);
+    static Akonadi::Item translateFromImap(Kolab::FolderType folderType, const Akonadi::Item &item, bool &ok);
+    static Akonadi::Item::List translateToImap(const Akonadi::Item::List &items, bool &ok);
+    static Akonadi::Item translateToImap(const Akonadi::Item &item, bool &ok);
+    static Kolab::FolderType folderTypeFromString(const QByteArray &folderTypeName);
+    static QByteArray getFolderTypeAnnotation(const QMap<QByteArray, QByteArray> &annotations);
+    static void setFolderTypeAnnotation(QMap<QByteArray, QByteArray> &annotations, const QByteArray &value);
+    static Kolab::ObjectType getKolabTypeFromMimeType(const QString &type);
+    static QByteArray kolabTypeForMimeType(const QStringList &contentMimeTypes);
+    static QStringList getContentMimeTypes(Kolab::FolderType type);
+    static QString getIcon(Kolab::FolderType type);
+    //Returns true if the folder type shouldn't be ignored
+    static bool isHandledType(Kolab::FolderType type);
+    static QString getMimeType(Kolab::FolderType type);
+    static QList<QByteArray> ancestorChain(const Akonadi::Collection &col);
+    static QString createMemberUrl(const Akonadi::Item &item, const QString &user);
+};
+
+#endif
diff --git a/resources/kolab/kolabmessagehelper.cpp b/resources/kolab/kolabmessagehelper.cpp
new file mode 100644 (file)
index 0000000..9fc8edd
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "kolabmessagehelper.h"
+
+#include <collectionannotationsattribute.h>
+#include <kolabdefinitions.h> //libkolab
+
+#include "kolabhelpers.h"
+#include "kolabresource_debug.h"
+
+KolabMessageHelper::KolabMessageHelper(const Akonadi::Collection &col)
+    : mCollection(col)
+{
+
+}
+
+KolabMessageHelper::~KolabMessageHelper()
+{
+
+}
+
+Akonadi::Item KolabMessageHelper::createItemFromMessage(KMime::Message::Ptr message,
+        const qint64 uid,
+        const qint64 size,
+        const QList<KIMAP::MessageAttribute> &attrs,
+        const QList<QByteArray> &flags,
+        const KIMAP::FetchJob::FetchScope &scope,
+        bool &ok) const
+{
+    const Akonadi::Item item = MessageHelper::createItemFromMessage(message, uid, size, attrs, flags, scope, ok);
+    if (!ok) {
+        qCWarning(KOLABRESOURCE_LOG) << "Failed to read imap message";
+        return item;
+    }
+    Kolab::FolderType folderType = Kolab::MailType;
+    if (mCollection.hasAttribute<Akonadi::CollectionAnnotationsAttribute>()) {
+        const QByteArray folderTypeString = KolabHelpers::getFolderTypeAnnotation(mCollection.attribute<Akonadi::CollectionAnnotationsAttribute>()->annotations());
+        folderType = KolabHelpers::folderTypeFromString(folderTypeString);
+    }
+    return KolabHelpers::translateFromImap(folderType, item, ok);
+}
diff --git a/resources/kolab/kolabmessagehelper.h b/resources/kolab/kolabmessagehelper.h
new file mode 100644 (file)
index 0000000..048b350
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef KOLABMESSAGEHELPER_H
+#define KOLABMESSAGEHELPER_H
+
+#include <messagehelper.h>
+#include <AkonadiCore/Collection>
+
+class KolabMessageHelper : public MessageHelper
+{
+public:
+    explicit KolabMessageHelper(const Akonadi::Collection &collection);
+    virtual ~KolabMessageHelper();
+    virtual Akonadi::Item createItemFromMessage(KMime::Message::Ptr message,
+            const qint64 uid,
+            const qint64 size,
+            const QList<KIMAP::MessageAttribute> &attrs,
+            const QList<QByteArray> &flags,
+            const KIMAP::FetchJob::FetchScope &scope,
+            bool &ok) const;
+
+private:
+    Akonadi::Collection mCollection;
+};
+
+#endif
diff --git a/resources/kolab/kolabrelationresourcetask.cpp b/resources/kolab/kolabrelationresourcetask.cpp
new file mode 100644 (file)
index 0000000..8d631ac
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Krammer <kevin.krammer@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "kolabrelationresourcetask.h"
+#include "kolabresource_debug.h"
+#include "kolabhelpers.h"
+
+#include <AkonadiCore/CollectionFetchJob>
+#include <AkonadiCore/CollectionFetchScope>
+#include <AkonadiCore/CollectionCreateJob>
+#include <kimap/createjob.h>
+#include <kimap/setmetadatajob.h>
+
+#include <KDE/KLocalizedString>
+
+KolabRelationResourceTask::KolabRelationResourceTask(ResourceStateInterface::Ptr resource, QObject *parent)
+    : ResourceTask(DeferIfNoSession, resource, parent)
+    , mImapSession(0)
+{
+}
+
+Akonadi::Collection KolabRelationResourceTask::relationCollection() const
+{
+    return mRelationCollection;
+}
+
+void KolabRelationResourceTask::doStart(KIMAP::Session *session)
+{
+    mImapSession = session;
+
+    // need to find the configuration collection.
+
+    Akonadi::Collection topLevelCollection;
+    topLevelCollection.setRemoteId(rootRemoteId());
+    topLevelCollection.setParentCollection(Akonadi::Collection::root());
+
+    Akonadi::CollectionFetchJob *fetchJob = new Akonadi::CollectionFetchJob(topLevelCollection, Akonadi::CollectionFetchJob::Recursive);
+    fetchJob->fetchScope().setResource(resourceState()->resourceIdentifier());
+    fetchJob->fetchScope().setContentMimeTypes(QStringList() << KolabHelpers::getMimeType(Kolab::ConfigurationType));
+    fetchJob->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All);
+    fetchJob->fetchScope().setListFilter(Akonadi::CollectionFetchScope::NoFilter);
+    connect(fetchJob, &KJob::result, this, &KolabRelationResourceTask::onCollectionFetchResult);
+}
+
+void KolabRelationResourceTask::onCollectionFetchResult(KJob *job)
+{
+    if (job->error() == 0) {
+        Akonadi::CollectionFetchJob *fetchJob = qobject_cast<Akonadi::CollectionFetchJob *>(job);
+        Q_ASSERT(fetchJob != 0);
+
+        Q_FOREACH (const Akonadi::Collection &collection, fetchJob->collections()) {
+            const QString mailBox = mailBoxForCollection(collection);
+            if (!mailBox.isEmpty()) {
+                mRelationCollection = collection;
+                startRelationTask(mImapSession);
+                return;
+            }
+        }
+    }
+
+    qCDebug(KOLABRESOURCE_LOG) << "Couldn't find collection for relations, creating one.";
+
+    const QChar separator = separatorCharacter();
+    mRelationCollection = Akonadi::Collection();
+    mRelationCollection.setName(QStringLiteral("Configuration"));
+    mRelationCollection.setContentMimeTypes(QStringList() << KolabHelpers::getMimeType(Kolab::ConfigurationType));
+    mRelationCollection.setRemoteId(separator + mRelationCollection.name());
+    const QString newMailBox = QStringLiteral("Configuration");
+    KIMAP::CreateJob *imapCreateJob = new KIMAP::CreateJob(mImapSession);
+    imapCreateJob->setMailBox(newMailBox);
+    connect(imapCreateJob, &KJob::result,
+            this, &KolabRelationResourceTask::onCreateDone);
+    imapCreateJob->start();
+}
+
+void KolabRelationResourceTask::onCreateDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << "Failed to create configuration folder: " << job->errorString();
+        cancelTask(i18n("Failed to create configuration folder on server"));
+        return;
+    }
+
+    KIMAP::SetMetaDataJob *setMetadataJob = new KIMAP::SetMetaDataJob(mImapSession);
+    if (serverCapabilities().contains(QStringLiteral("METADATA"))) {
+        setMetadataJob->setServerCapability(KIMAP::MetaDataJobBase::Metadata);
+    } else {
+        setMetadataJob->setServerCapability(KIMAP::MetaDataJobBase::Annotatemore);
+    }
+    setMetadataJob->setMailBox(QStringLiteral("Configuration"));
+    setMetadataJob->addMetaData("/shared/vendor/kolab/folder-type", "configuration.default");
+    connect(setMetadataJob, &KJob::result,
+            this, &KolabRelationResourceTask::onSetMetaDataDone);
+    setMetadataJob->start();
+}
+
+void KolabRelationResourceTask::onSetMetaDataDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << "Failed to write annotations: " << job->errorString();
+        cancelTask(i18n("Failed to write some annotations for '%1' on the IMAP server. %2",
+                        collection().name(), job->errorText()));
+        return;
+    }
+
+    Akonadi::CollectionCreateJob *createJob = new Akonadi::CollectionCreateJob(mRelationCollection, this);
+    connect(createJob, &KJob::result, this, &KolabRelationResourceTask::onLocalCreateDone);
+}
+
+void KolabRelationResourceTask::onLocalCreateDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << "Failed to create local folder: " << job->errorString();
+        cancelTask(i18n("Failed to create configuration folder"));
+        return;
+    }
+    mRelationCollection = static_cast<Akonadi::CollectionCreateJob *>(job)->collection();
+    startRelationTask(mImapSession);
+}
+
diff --git a/resources/kolab/kolabrelationresourcetask.h b/resources/kolab/kolabrelationresourcetask.h
new file mode 100644 (file)
index 0000000..b35e74c
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Krammer <kevin.krammer@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef KOLABRELATIONRESOURCETASK_H
+#define KOLABRELATIONRESOURCETASK_H
+
+#include <resourcetask.h>
+
+class KolabRelationResourceTask : public ResourceTask
+{
+    Q_OBJECT
+public:
+    explicit KolabRelationResourceTask(ResourceStateInterface::Ptr resource, QObject *parent = Q_NULLPTR);
+
+    Akonadi::Collection relationCollection() const;
+
+    using ResourceTask::mailBoxForCollection;
+    using ResourceTask::resourceState;
+
+protected:
+    Akonadi::Collection mRelationCollection;
+
+protected:
+    virtual void doStart(KIMAP::Session *session);
+
+    virtual void startRelationTask(KIMAP::Session *session) = 0;
+
+private:
+    KIMAP::Session *mImapSession;
+
+private Q_SLOTS:
+    void onCollectionFetchResult(KJob *job);
+    void onCreateDone(KJob *job);
+    void onSetMetaDataDone(KJob *job);
+    void onLocalCreateDone(KJob *job);
+};
+
+#endif // KOLABRELATIONRESOURCETASK_H
diff --git a/resources/kolab/kolabremovetagtask.cpp b/resources/kolab/kolabremovetagtask.cpp
new file mode 100644 (file)
index 0000000..e4acc35
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Krammer <kevin.krammer@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "kolabremovetagtask.h"
+#include "kolabresource_debug.h"
+#include <imapflags.h>
+
+#include <kimap/selectjob.h>
+#include <kimap/session.h>
+#include <kimap/storejob.h>
+#include "tracer.h"
+
+KolabRemoveTagTask::KolabRemoveTagTask(ResourceStateInterface::Ptr resource, QObject *parent)
+    : KolabRelationResourceTask(resource, parent)
+{
+}
+
+void KolabRemoveTagTask::startRelationTask(KIMAP::Session *session)
+{
+    // The imap specs do not allow for a single message to be deleted. We can only
+    // set the \Deleted flag. The message will actually be deleted when EXPUNGE will
+    // be issued on the next retrieveItems().
+
+    const QString mailBox = mailBoxForCollection(relationCollection());
+
+    Trace() << mailBox;
+    qCDebug(KOLABRESOURCE_LOG) << "Deleting tag " << resourceState()->tag().name() << " from " << mailBox;
+
+    if (session->selectedMailBox() != mailBox) {
+        KIMAP::SelectJob *select = new KIMAP::SelectJob(session);
+        select->setMailBox(mailBox);
+
+        connect(select, &KJob::result,
+                this, &KolabRemoveTagTask::onSelectDone);
+
+        select->start();
+
+    } else {
+        triggerStoreJob(session);
+    }
+}
+
+void KolabRemoveTagTask::triggerStoreJob(KIMAP::Session *session)
+{
+    KIMAP::ImapSet set;
+    set.add(resourceState()->tag().remoteId().toLong());
+    Trace() << set.toImapSequenceSet();
+
+    KIMAP::StoreJob *store = new KIMAP::StoreJob(session);
+    store->setUidBased(true);
+    store->setSequenceSet(set);
+    store->setFlags(QList<QByteArray>() << ImapFlags::Deleted);
+    store->setMode(KIMAP::StoreJob::AppendFlags);
+    connect(store, &KJob::result, this, &KolabRemoveTagTask::onStoreFlagsDone);
+    store->start();
+}
+
+void KolabRemoveTagTask::onSelectDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << "Failed to select mailbox: " << job->errorString();
+        cancelTask(job->errorString());
+    } else {
+        KIMAP::SelectJob *select = static_cast<KIMAP::SelectJob *>(job);
+        triggerStoreJob(select->session());
+    }
+}
+
+void KolabRemoveTagTask::onStoreFlagsDone(KJob *job)
+{
+    Trace();
+    //TODO use UID EXPUNGE if available
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << "Failed to append flags: " << job->errorString();
+        cancelTask(job->errorString());
+    } else {
+        changeProcessed();
+    }
+}
diff --git a/resources/kolab/kolabremovetagtask.h b/resources/kolab/kolabremovetagtask.h
new file mode 100644 (file)
index 0000000..fa382f7
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Krammer <kevin.krammer@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef KOLABREMOVETAGTASK_H
+#define KOLABREMOVETAGTASK_H
+
+#include "kolabrelationresourcetask.h"
+
+class KolabRemoveTagTask : public KolabRelationResourceTask
+{
+    Q_OBJECT
+public:
+    explicit KolabRemoveTagTask(ResourceStateInterface::Ptr resource, QObject *parent = Q_NULLPTR);
+
+protected:
+    virtual void startRelationTask(KIMAP::Session *session);
+
+private:
+    void triggerStoreJob(KIMAP::Session *session);
+
+private Q_SLOTS:
+    void onSelectDone(KJob *job);
+    void onStoreFlagsDone(KJob *job);
+};
+
+#endif // KOLABREMOVETAGTASK_H
diff --git a/resources/kolab/kolabresource.cpp b/resources/kolab/kolabresource.cpp
new file mode 100644 (file)
index 0000000..ad5f30d
--- /dev/null
@@ -0,0 +1,234 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "kolabresource.h"
+
+#include "kolabresource_debug.h"
+#include "setupserver.h"
+#include <resourcestateinterface.h>
+#include <resourcestate.h>
+#include <retrieveitemstask.h>
+#include <collectionannotationsattribute.h>
+#include <changecollectiontask.h>
+#include <akonadi/calendar/blockalarmsattribute.h>
+
+#include <KWindowSystem>
+#include <KLocalizedString>
+#include <QIcon>
+
+#include "kolabretrievetagstask.h"
+#include "kolabresourcestate.h"
+#include "kolabhelpers.h"
+#include "kolabsettings.h"
+#include "kolabaddtagtask.h"
+#include "kolabchangeitemstagstask.h"
+#include "kolabchangeitemsrelationstask.h"
+#include "kolabchangetagtask.h"
+#include "kolabremovetagtask.h"
+#include "kolabretrievecollectionstask.h"
+#include "kolabretrievetagstask.h"
+#include "tracer.h"
+
+KolabResource::KolabResource(const QString &id)
+    : ImapResource(id)
+{
+    //Ensure we have up-to date metadata before attempting to sync folder
+    setScheduleAttributeSyncBeforeItemSync(true);
+    setKeepLocalCollectionChanges(QSet<QByteArray>() << "ENTITYDISPLAY" << Akonadi::BlockAlarmsAttribute().type());
+}
+
+KolabResource::~KolabResource()
+{
+
+}
+
+Settings *KolabResource::settings() const
+{
+    if (m_settings == 0) {
+        m_settings = new KolabSettings;
+    }
+
+    return m_settings;
+}
+
+void KolabResource::delayedInit()
+{
+    ImapResource::delayedInit();
+    settings()->setRetrieveMetadataOnFolderListing(false);
+    Q_ASSERT(!settings()->retrieveMetadataOnFolderListing());
+}
+
+QString KolabResource::defaultName() const
+{
+    return i18n("Kolab Resource");
+}
+
+QDialog *KolabResource::createConfigureDialog(WId windowId)
+{
+    SetupServer *dlg = new SetupServer(this, windowId);
+    KWindowSystem::setMainWindow(dlg, windowId);
+    dlg->setWindowIcon(QIcon::fromTheme(QStringLiteral("kolab")));
+    connect(dlg, SIGNAL(finished(int)), this, SLOT(onConfigurationDone(int)));;
+    return dlg;
+}
+
+ResourceStateInterface::Ptr KolabResource::createResourceState(const TaskArguments &args)
+{
+    return ResourceStateInterface::Ptr(new KolabResourceState(this, args));
+}
+
+void KolabResource::retrieveCollections()
+{
+    Trace();
+    emit status(AgentBase::Running, i18nc("@info:status", "Retrieving folders"));
+
+    startTask(new KolabRetrieveCollectionsTask(createResourceState(TaskArguments()), this));
+    synchronizeTags();
+    synchronizeRelations();
+}
+
+void KolabResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection)
+{
+    Trace() << item.id() << collection.id();
+    bool ok = true;
+    const Akonadi::Item imapItem = KolabHelpers::translateToImap(item, ok);
+    if (!ok) {
+        qCWarning(KOLABRESOURCE_LOG) << "Failed to convert item";
+        cancelTask();
+        return;
+    }
+    ImapResource::itemAdded(imapItem, collection);
+}
+
+void KolabResource::itemChanged(const Akonadi::Item &item, const QSet< QByteArray > &parts)
+{
+    Trace() << item.id() << parts;
+    bool ok = true;
+    const Akonadi::Item imapItem = KolabHelpers::translateToImap(item, ok);
+    if (!ok) {
+        qCWarning(KOLABRESOURCE_LOG) << "Failed to convert item";
+        cancelTask();
+        return;
+    }
+    ImapResource::itemChanged(imapItem, parts);
+}
+
+void KolabResource::itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &source, const Akonadi::Collection &destination)
+{
+    Trace() << items.size() << source.id() << destination.id();
+    bool ok = true;
+    const Akonadi::Item::List imapItems = KolabHelpers::translateToImap(items, ok);
+    if (!ok) {
+        qCWarning(KOLABRESOURCE_LOG) << "Failed to convert item";
+        cancelTask();
+        return;
+    }
+    ImapResource::itemsMoved(imapItems, source, destination);
+}
+
+static Akonadi::Collection updateAnnotations(const Akonadi::Collection &collection)
+{
+    Trace() << collection.id();
+    //Set the annotations on new folders
+    const QByteArray kolabType = KolabHelpers::kolabTypeForMimeType(collection.contentMimeTypes());
+    if (!kolabType.isEmpty()) {
+        Akonadi::Collection col = collection;
+        Akonadi::CollectionAnnotationsAttribute *attr = col.attribute<Akonadi::CollectionAnnotationsAttribute>(Akonadi::Collection::AddIfMissing);
+        QMap<QByteArray, QByteArray> annotations = attr->annotations();
+        KolabHelpers::setFolderTypeAnnotation(annotations, kolabType);
+        attr->setAnnotations(annotations);
+        return col;
+    }
+    return collection;
+}
+
+void KolabResource::collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent)
+{
+    Trace() << collection.id() << parent.id();
+    //Set the annotations on new folders
+    const Akonadi::Collection col = updateAnnotations(collection);
+    //TODO we need to save the collections as well if the annotations have changed
+    //or we simply don't have the annotations locally, which perhaps is also not required?
+    ImapResource::collectionAdded(col, parent);
+}
+
+void KolabResource::collectionChanged(const Akonadi::Collection &collection, const QSet< QByteArray > &parts)
+{
+    Trace() << collection.id() << parts;
+    //Update annotations if necessary
+    const Akonadi::Collection col = updateAnnotations(collection);
+    //TODO we need to save the collections as well if the annotations have changed
+    emit status(AgentBase::Running, i18nc("@info:status", "Updating folder '%1'", collection.name()));
+    ChangeCollectionTask *task = new ChangeCollectionTask(createResourceState(TaskArguments(collection, parts)), this);
+    task->syncEnabledState(true);
+    startTask(task);
+}
+
+void KolabResource::tagAdded(const Akonadi::Tag &tag)
+{
+    Trace() << tag.id();
+    KolabAddTagTask *task = new KolabAddTagTask(createResourceState(TaskArguments(tag)), this);
+    startTask(task);
+}
+
+void KolabResource::tagChanged(const Akonadi::Tag &tag)
+{
+    Trace() << tag.id();
+    KolabChangeTagTask *task = new KolabChangeTagTask(createResourceState(TaskArguments(tag)), QSharedPointer<TagConverter>(new TagConverter), this);
+    startTask(task);
+}
+
+void KolabResource::tagRemoved(const Akonadi::Tag &tag)
+{
+    Trace() << tag.id();
+    KolabRemoveTagTask *task = new KolabRemoveTagTask(createResourceState(TaskArguments(tag)), this);
+    startTask(task);
+}
+
+void KolabResource::itemsTagsChanged(const Akonadi::Item::List &items, const QSet<Akonadi::Tag> &addedTags, const QSet<Akonadi::Tag> &removedTags)
+{
+    Trace() << items.size() << addedTags.size() << removedTags.size();
+    KolabChangeItemsTagsTask *task = new KolabChangeItemsTagsTask(createResourceState(TaskArguments(items, addedTags, removedTags)), QSharedPointer<TagConverter>(new TagConverter), this);
+    startTask(task);
+}
+
+void KolabResource::retrieveTags()
+{
+    Trace();
+    KolabRetrieveTagTask *task = new KolabRetrieveTagTask(createResourceState(TaskArguments()), KolabRetrieveTagTask::RetrieveTags, this);
+    startTask(task);
+}
+
+void KolabResource::retrieveRelations()
+{
+    Trace();
+    KolabRetrieveTagTask *task = new KolabRetrieveTagTask(createResourceState(TaskArguments()), KolabRetrieveTagTask::RetrieveRelations, this);
+    startTask(task);
+}
+
+void KolabResource::itemsRelationsChanged(const Akonadi::Item::List &items,
+        const Akonadi::Relation::List &addedRelations,
+        const Akonadi::Relation::List &removedRelations)
+{
+    Trace() << items.size() << addedRelations.size() << removedRelations.size();
+    KolabChangeItemsRelationsTask *task = new KolabChangeItemsRelationsTask(createResourceState(TaskArguments(items, addedRelations, removedRelations)));
+    startTask(task);
+}
+
+AKONADI_RESOURCE_MAIN(KolabResource)
diff --git a/resources/kolab/kolabresource.desktop b/resources/kolab/kolabresource.desktop
new file mode 100644 (file)
index 0000000..1524ac8
--- /dev/null
@@ -0,0 +1,88 @@
+[Desktop Entry]
+Name=Kolab Groupware Server
+Name[bg]=Сървър Kolab Groupware
+Name[bs]=Server kolaborativnog softvera
+Name[ca]=Servidor de treball en grup Kolab
+Name[ca@valencia]=Servidor de treball en grup Kolab
+Name[cs]=Kolab Groupware server
+Name[da]=Kolab groupware-server
+Name[de]=Kolab Groupware-Server
+Name[el]=Εξυπηρετητής Groupware Kolab
+Name[en_GB]=Kolab Groupware Server
+Name[es]=Servidor de colaboración Kolab
+Name[et]=Kolabi grupitöö server
+Name[fi]=Kolab-työryhmäpalvelin
+Name[fr]=Serveur de logiciels de collaboration Kolab
+Name[ga]=Freastalaí Groupware Kolab
+Name[gl]=Servidor de Traballo en Grupo Kolab
+Name[hu]=Kolab csoportmunka-kiszolgáló
+Name[ia]=Servitor de Kolab Groupware
+Name[it]=Server di groupware Kolab
+Name[ja]=Kolab グループウェアサーバ
+Name[kk]=Kolab топтық іс сервері
+Name[km]=ម៉ាស៊ីន​បម្រើ Kolab Groupware
+Name[ko]=Kolab 그룹웨어 서버
+Name[lt]=Kolab grupinio darbo serveris
+Name[lv]=Kolab grupdarba serveris
+Name[nb]=Kolab groupware-tjener
+Name[nds]=Kolab-Arbeitkoppelserver
+Name[nl]=Kolab groupwareserver
+Name[nn]=Kolab Groupware-tenar
+Name[pl]=Serwer Groupware Kolab
+Name[pt]=Servidor de Groupware Kolab
+Name[pt_BR]=Servidor groupware Kolab
+Name[ro]=Server Kolab Groupware
+Name[ru]=Сервер совместной работы Kolab
+Name[sk]=Groupware Server Kolab
+Name[sl]=Strežnik za skupinsko delo Kolab
+Name[sr]=Колабов групверски сервер
+Name[sr@ijekavian]=Колабов групверски сервер
+Name[sr@ijekavianlatin]=Kolabov grupverski server
+Name[sr@latin]=Kolabov grupverski server
+Name[sv]=Kolab grupprogramserver
+Name[tr]=Kolab Groupware Sunucusu
+Name[uk]=Сервер групової роботи Kolab
+Name[x-test]=xxKolab Groupware Serverxx
+Name[zh_CN]=Kolab 群件服务器
+Name[zh_TW]=Kolab 群組伺服器
+Comment=Provides access to Kolab groupware folders and e-mail on a Kolab IMAP Server.
+Comment[ca]=Proporciona accés a les carpetes i al correu electrònic del treball en grup Kolab en un servidor IMAP del Kolab.
+Comment[ca@valencia]=Proporciona accés a les carpetes i al correu electrònic del treball en grup Kolab en un servidor IMAP del Kolab.
+Comment[da]=Giver adgang til Kolab groupware-mapper og e-mail på en Kolab IMAP-server.
+Comment[de]=Ermöglicht den Zugriff auf Kolab-Groupware-Ordner auf einem Kolab-IMAP-Server.
+Comment[el]=Προσφέρει πρόσβαση σε φακέλους Kolab groupware και ηλ.αλληλογραφία σε έναν εξυπηρετητή Kolab IMAP.
+Comment[en_GB]=Provides access to Kolab groupware folders and e-mail on a Kolab IMAP Server.
+Comment[es]=Proporciona acceso a carpetas de colaboración de Kolab y correo en un servidor IMAP Kolab.
+Comment[et]=Ligipääsu tagamine Kolabi grupitöökaustadele ja kirjadele Kolabi IMAP serveris.
+Comment[fi]=Tarjoaa pääsyn Kolab-työryhmäkansioihin ja sähköpostiin Kolab IMAP-palvelimella.
+Comment[fr]=Fournit l'accès aux dossiers et aux courriels du logiciel de collaboration Kolab sur un serveur IMAP.
+Comment[gl]=Fornece acceso aos cartafoles de traballo en grupo de Kolab dun servidor de IMAP.
+Comment[hu]=Hozzáférést biztosít a Kolab csoportmunka-mappákhoz és e-mailekhez egy Kolab IMAP kiszolgálón.
+Comment[it]=Fornisce l'accesso a cartelle e messaggi di posta di groupware Kolab su un server Kolab IMAP.
+Comment[ko]=Kolab 그룹웨어 폴더 및 Kolab IMAP 서버에 접근합니다
+Comment[nb]=Gir tilgang til Kolab gruppevaremapper og e-post på en KOLAB IMAP-tjener.
+Comment[nds]=Stellt Togriep op Kolab-Arbeitkoppelornern un Nettpost op en Kolab-IMAP-Server praat.
+Comment[nl]=Levert toegang tot Kolab groupwaremappen en e-mail op een Kolab IMAP-server.
+Comment[pl]=Zapewnia dostęp do katalogów do pracy grupowej Kolab i poczty elektronicznej na serwerze Kolab IMAP.
+Comment[pt]=Oferece o acesso às pastas de 'groupware' e de e-mail num servidor IMAP do Kolab.
+Comment[pt_BR]=Fornece acesso as pastas groupware Kolab em de e-mail em um servidor IMAP Kolab.
+Comment[ru]=Доступ к папкам совместной работы Kolab и к электронной почте на сервере IMAP от Kolab.
+Comment[sk]=Poskytuje prístup k priečinkom Kolab groupware a e-mailu na Kolab IMAP serveri.
+Comment[sl]=Omogoča dostop do Kolabovih map za skupinsko delo na strežniku IMAP.
+Comment[sr]=Омогућава приступ Колабовим групверским фасциклама и е‑пошти на Колабовом ИМАП серверу.
+Comment[sr@ijekavian]=Омогућава приступ Колабовим групверским фасциклама и е‑пошти на Колабовом ИМАП серверу.
+Comment[sr@ijekavianlatin]=Omogućava pristup Kolabovim grupverskim fasciklama i e‑pošti na Kolabovom IMAP serveru.
+Comment[sr@latin]=Omogućava pristup Kolabovim grupverskim fasciklama i e‑pošti na Kolabovom IMAP serveru.
+Comment[sv]=Ger tillgång till Kolab grupprogramkorgar och e-post på en Kolab IMAP-server.
+Comment[tr]=Kolab grup yazılımı klasörlerine ve Kolab IMAP Sunucusu'na erişim sağlar
+Comment[uk]=Надає доступ до тек групової роботи та електронної пошти на серверів IMAP Kolab.
+Comment[x-test]=xxProvides access to Kolab groupware folders and e-mail on a Kolab IMAP Server.xx
+Comment[zh_CN]=提供了对 Kolab 群件文件夹 和 Kolab IMAP 服务器上的电子邮件的访问功能。
+Comment[zh_TW]=提供存取某 Kolab IMAP 伺服器上的 Kolab 群組資料夾與電子郵件
+Type=AkonadiResource
+Exec=akonadi_kolab_resource
+Icon=kolab
+
+X-Akonadi-MimeTypes=message/rfc822,text/directory,text/calendar,application/x-vnd.kde.contactgroup,application/x-vnd.akonadi.calendar.event,application/x-vnd.akonadi.calendar.todo,application/x-vnd.akonadi.calendar.journal,application/x-vnd.akonadi.calendar.freebusy,text/x-vnd.akonadi.note
+X-Akonadi-Capabilities=Resource
+X-Akonadi-Identifier=akonadi_kolab_resource
diff --git a/resources/kolab/kolabresource.h b/resources/kolab/kolabresource.h
new file mode 100644 (file)
index 0000000..2f22dfd
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef KOLABRESOURCE_H
+#define KOLABRESOURCE_H
+
+#include "imapresource.h"
+#include <resourcestate.h>
+
+class KolabResource : public ImapResource
+{
+    Q_OBJECT
+
+    using Akonadi::AgentBase::Observer::collectionChanged;
+
+public:
+    explicit KolabResource(const QString &id);
+    ~KolabResource();
+
+    QDialog *createConfigureDialog(WId windowId) Q_DECL_OVERRIDE;
+    Settings *settings() const Q_DECL_OVERRIDE;
+
+protected Q_SLOTS:
+    void retrieveCollections() Q_DECL_OVERRIDE;
+    void delayedInit();
+
+protected:
+    ResourceStateInterface::Ptr createResourceState(const TaskArguments &) Q_DECL_OVERRIDE;
+
+    void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    void itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+    void itemsMoved(const Akonadi::Item::List &item, const Akonadi::Collection &source,
+                    const Akonadi::Collection &destination) Q_DECL_OVERRIDE;
+    //itemsRemoved and itemsFlags changed do not require translation, because they don't use the payload
+
+    void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) Q_DECL_OVERRIDE;
+    void collectionChanged(const Akonadi::Collection &collection, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+    //collectionRemoved & collectionMoved do not require adjustments since they don't change the annotations
+
+    void tagAdded(const Akonadi::Tag &tag) Q_DECL_OVERRIDE;
+    void tagChanged(const Akonadi::Tag &tag) Q_DECL_OVERRIDE;
+    void tagRemoved(const Akonadi::Tag &tag) Q_DECL_OVERRIDE;
+    void itemsTagsChanged(const Akonadi::Item::List &items, const QSet<Akonadi::Tag> &addedTags, const QSet<Akonadi::Tag> &removedTags) Q_DECL_OVERRIDE;
+
+    void itemsRelationsChanged(const Akonadi::Item::List &items,
+                               const Akonadi::Relation::List &addedRelations,
+                               const Akonadi::Relation::List &removedRelations) Q_DECL_OVERRIDE;
+
+    QString defaultName() const Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void retrieveTags();
+    void retrieveRelations();
+};
+
+#endif
diff --git a/resources/kolab/kolabresourcestate.cpp b/resources/kolab/kolabresourcestate.cpp
new file mode 100644 (file)
index 0000000..e375d8b
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "kolabresourcestate.h"
+#include "kolabhelpers.h"
+#include "kolabmessagehelper.h"
+
+#include <imapresource.h>
+
+#include <collectionannotationsattribute.h>
+#include <AkonadiCore/EntityDisplayAttribute>
+#include <AkonadiCore/CachePolicy>
+#include <noselectattribute.h>
+#include <Akonadi/KMime/MessageParts>
+#include "kolabresource_debug.h"
+
+KolabResourceState::KolabResourceState(ImapResource *resource, const TaskArguments &arguments)
+    : ResourceState(resource, arguments)
+{
+
+}
+
+static Akonadi::Collection processAnnotations(const Akonadi::Collection &collection)
+{
+    if (collection.attribute<Akonadi::CollectionAnnotationsAttribute>()) {
+        Akonadi::Collection col = collection;
+        const QMap<QByteArray, QByteArray> rawAnnotations = col.attribute<Akonadi::CollectionAnnotationsAttribute>()->annotations();
+        const QByteArray type = KolabHelpers::getFolderTypeAnnotation(rawAnnotations);
+        const Kolab::FolderType folderType = KolabHelpers::folderTypeFromString(type);
+        col.setContentMimeTypes(KolabHelpers::getContentMimeTypes(folderType));
+
+        const QString icon = KolabHelpers::getIcon(folderType);
+        if (!icon.isEmpty()) {
+            // qCDebug(KOLABRESOURCE_LOG) << " setting icon " << icon;
+            Akonadi::EntityDisplayAttribute *attr = col.attribute<Akonadi::EntityDisplayAttribute>(Akonadi::Collection::AddIfMissing);
+            attr->setIconName(icon);
+        }
+        if (folderType != Kolab::MailType) {
+            //Groupware data always requires the full message, because it cannot translate without the body
+            Akonadi::CachePolicy cachePolicy = col.cachePolicy();
+            QStringList localParts = cachePolicy.localParts();
+            if (!localParts.contains(QLatin1String(Akonadi::MessagePart::Body))) {
+                localParts << QLatin1String(Akonadi::MessagePart::Body);
+                cachePolicy.setLocalParts(localParts);
+                cachePolicy.setCacheTimeout(-1);
+                cachePolicy.setInheritFromParent(false);
+                cachePolicy.setSyncOnDemand(true);
+                col.setCachePolicy(cachePolicy);
+            }
+        }
+        if (folderType == Kolab::ConfigurationType) {
+            //we want to hide this folder from indexing and display, but still have the data available locally.
+            col.setEnabled(false);
+            col.setShouldList(Akonadi::Collection::ListSync, true);
+        }
+        if (!KolabHelpers::isHandledType(folderType)) {
+            //If we don't handle the folder, make sure we don't download the messages
+            col.attribute<NoSelectAttribute>(Akonadi::Collection::AddIfMissing);
+        }
+        return col;
+    }
+    return collection;
+}
+
+void KolabResourceState::collectionAttributesRetrieved(const Akonadi::Collection &collection)
+{
+    if (!collection.isValid() && collection.remoteId().isEmpty()) {
+        ResourceState::collectionAttributesRetrieved(collection);
+        return;
+    }
+    const Akonadi::Collection col = processAnnotations(collection);
+    ResourceState::collectionAttributesRetrieved(col);
+}
+
+void KolabResourceState::collectionsRetrieved(const Akonadi::Collection::List &collections)
+{
+    Akonadi::Collection::List modifiedCollections;
+    Q_FOREACH (const Akonadi::Collection &col, collections) {
+        modifiedCollections << processAnnotations(col);
+    }
+    ResourceState::collectionsRetrieved(modifiedCollections);
+}
+
+MessageHelper::Ptr KolabResourceState::messageHelper() const
+{
+    return MessageHelper::Ptr(new KolabMessageHelper(collection()));
+}
diff --git a/resources/kolab/kolabresourcestate.h b/resources/kolab/kolabresourcestate.h
new file mode 100644 (file)
index 0000000..ae35ac8
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef KOLABRESOURCESTATE_H
+#define KOLABRESOURCESTATE_H
+
+#include <resourcestate.h>
+
+class ImapResource;
+
+class KolabResourceState : public ::ResourceState
+{
+public:
+    explicit KolabResourceState(ImapResource *resource, const TaskArguments &arguments);
+
+private:
+    virtual void collectionAttributesRetrieved(const Akonadi::Collection &collection);
+    virtual void collectionsRetrieved(const Akonadi::Collection::List &collections);
+    virtual MessageHelper::Ptr messageHelper() const;
+};
+
+#endif
diff --git a/resources/kolab/kolabretrievecollectionstask.cpp b/resources/kolab/kolabretrievecollectionstask.cpp
new file mode 100644 (file)
index 0000000..6bfc44a
--- /dev/null
@@ -0,0 +1,534 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "kolabretrievecollectionstask.h"
+#include "kolabhelpers.h"
+#include "tracer.h"
+#include "kolabresource_debug.h"
+
+#include <noselectattribute.h>
+#include <noinferiorsattribute.h>
+#include <collectionannotationsattribute.h>
+#include <collectionmetadatahelper.h>
+#include <imapaclattribute.h>
+#include <kimap/getmetadatajob.h>
+#include <kimap/myrightsjob.h>
+
+#include <AkonadiCore/CachePolicy>
+#include <AkonadiCore/EntityDisplayAttribute>
+#include <akonadi/kmime/messageparts.h>
+#include <AkonadiCore/CollectionIdentificationAttribute>
+#include <akonadi/calendar/blockalarmsattribute.h>
+#include <AkonadiCore/vectorhelper.h>
+
+#include <kmime/kmime_message.h>
+#include <KLocalizedString>
+
+static bool isNamespaceFolder(const QString &path, const QList<KIMAP::MailBoxDescriptor> &namespaces, bool matchCompletePath = false)
+{
+    Q_FOREACH (const KIMAP::MailBoxDescriptor &desc, namespaces) {
+        if (path.startsWith(desc.name.left(desc.name.size() - 1))) { //Namespace ends with path separator and pathPart doesn't
+            if (!matchCompletePath || path.size() - desc.name.size() <= 1) {      //We want to match only for the complete path
+                return true;
+            }
+        }
+    }
+    return false;
+}
+
+RetrieveMetadataJob::RetrieveMetadataJob(KIMAP::Session *session, const QStringList &mailboxes, const QStringList &serverCapabilities, const QSet<QByteArray> &requestedMetadata, const QString &separator, const QList<KIMAP::MailBoxDescriptor> &sharedNamespace, const QList<KIMAP::MailBoxDescriptor> &userNamespace, QObject *parent)
+    : KJob(parent)
+    , mJobs(0)
+    , mRequestedMetadata(requestedMetadata)
+    , mServerCapabilities(serverCapabilities)
+    , mMailboxes(mailboxes)
+    , mSession(session)
+    , mSeparator(separator)
+    , mSharedNamespace(sharedNamespace)
+    , mUserNamespace(userNamespace)
+{
+
+}
+
+void RetrieveMetadataJob::start()
+{
+    Trace();
+    //Fill the map with empty entires so we set the mimetype to mail if no metadata is retrieved
+    Q_FOREACH (const QString &mailbox, mMailboxes) {
+        mMetadata.insert(mailbox, QMap<QByteArray, QByteArray>());
+    }
+
+    if (mServerCapabilities.contains(QStringLiteral("METADATA")) || mServerCapabilities.contains(QStringLiteral("ANNOTATEMORE"))) {
+        QSet<QString> toplevelMailboxes;
+        Q_FOREACH (const QString &mailbox, mMailboxes) {
+            const QStringList parts = mailbox.split(mSeparator);
+            if (!parts.isEmpty()) {
+                if (isNamespaceFolder(mailbox, mUserNamespace) && parts.length() >= 2) {
+                    // Other Users can be too big to request with a single command so we request Other Users/<user>/*
+                    toplevelMailboxes << parts.at(0) + mSeparator + parts.at(1) + mSeparator;
+                } else if (!isNamespaceFolder(mailbox, mSharedNamespace)) {
+                    toplevelMailboxes << parts.first();
+                }
+            }
+        }
+        Q_FOREACH (const KIMAP::MailBoxDescriptor &desc, mSharedNamespace) {
+            toplevelMailboxes << desc.name;
+        }
+        //TODO perhaps exclude the shared and other users namespaces by listing only toplevel (with %), and then only getting metadata of the toplevel folders.
+        Q_FOREACH (const QString &mailbox, toplevelMailboxes) {
+            {
+                KIMAP::GetMetaDataJob *meta = new KIMAP::GetMetaDataJob(mSession);
+                meta->setMailBox(mailbox + QLatin1String("*"));
+                if (mServerCapabilities.contains(QStringLiteral("METADATA"))) {
+                    meta->setServerCapability(KIMAP::MetaDataJobBase::Metadata);
+                } else {
+                    meta->setServerCapability(KIMAP::MetaDataJobBase::Annotatemore);
+                }
+                meta->setDepth(KIMAP::GetMetaDataJob::AllLevels);
+                Q_FOREACH (const QByteArray &requestedEntry, mRequestedMetadata) {
+                    meta->addRequestedEntry(requestedEntry);
+                }
+                connect(meta, &KJob::result, this, &RetrieveMetadataJob::onGetMetaDataDone);
+                mJobs++;
+                meta->start();
+            }
+        }
+    }
+
+    // Get the ACLs from the mailbox if it's supported
+    if (mServerCapabilities.contains(QStringLiteral("ACL"))) {
+
+        Q_FOREACH (const QString &mailbox, mMailboxes) {
+            // "Shared Folders" is not a valid mailbox, so we have to skip the ACL request for this folder
+            if (isNamespaceFolder(mailbox, mSharedNamespace, true)) {
+                continue;
+            }
+            KIMAP::MyRightsJob *rights = new KIMAP::MyRightsJob(mSession);
+            rights->setMailBox(mailbox);
+            connect(rights, &KJob::result, this, &RetrieveMetadataJob::onRightsReceived);
+            mJobs++;
+            rights->start();
+        }
+    }
+    checkDone();
+}
+
+void RetrieveMetadataJob::onGetMetaDataDone(KJob *job)
+{
+    mJobs--;
+    KIMAP::GetMetaDataJob *meta = static_cast<KIMAP::GetMetaDataJob *>(job);
+    if (job->error()) {
+        qCDebug(KOLABRESOURCE_LOG) << "No metadata for for mailbox: " << meta->mailBox();
+        if (!isNamespaceFolder(meta->mailBox(), mSharedNamespace)) {
+            qCWarning(KOLABRESOURCE_LOG) << "Get metadata failed: " << job->errorString();
+            //We ignore the error to avoid failing the complete sync. We can run into this when trying to retrieve rights for non-existing mailboxes.
+        }
+        checkDone();
+        return;
+    }
+
+    const QHash<QString, QMap<QByteArray, QByteArray> > metadata = meta->allMetaDataForMailboxes();
+    Q_FOREACH (const QString &folder, metadata.keys()) {
+        mMetadata.insert(folder, metadata.value(folder));
+    }
+    checkDone();
+}
+
+void RetrieveMetadataJob::onRightsReceived(KJob *job)
+{
+    mJobs--;
+    KIMAP::MyRightsJob *rights = static_cast<KIMAP::MyRightsJob *>(job);
+    if (job->error()) {
+        qCDebug(KOLABRESOURCE_LOG) << "No rights for mailbox: " << rights->mailBox();
+        if (!isNamespaceFolder(rights->mailBox(), mSharedNamespace)) {
+            qCWarning(KOLABRESOURCE_LOG) << "MyRights failed: " << job->errorString();
+            //We ignore the error to avoid failing the complete sync. We can run into this when trying to retrieve rights for non-existing mailboxes.
+        }
+        checkDone();
+        return;
+    }
+
+    const KIMAP::Acl::Rights imapRights = rights->rights();
+    mRights.insert(rights->mailBox(), imapRights);
+    checkDone();
+}
+
+void RetrieveMetadataJob::checkDone()
+{
+    if (!mJobs) {
+        Trace() << "done";
+        qCDebug(KOLABRESOURCE_LOG) << "done";
+        emitResult();
+    }
+}
+
+KolabRetrieveCollectionsTask::KolabRetrieveCollectionsTask(ResourceStateInterface::Ptr resource, QObject *parent)
+    : ResourceTask(CancelIfNoSession, resource, parent)
+    , mJobs(0)
+    , mSession(Q_NULLPTR)
+    , cContentMimeTypes("CONTENTMIMETYPES")
+    , cAccessRights("AccessRights")
+    , cImapAcl("imapacl")
+    , cCollectionAnnotations("collectionannotations")
+    , cDefaultKeepLocalChanges(QSet<QByteArray>() << cContentMimeTypes << cAccessRights << cImapAcl << cCollectionAnnotations)
+    , cDefaultMimeTypes(QStringList() << Akonadi::Collection::mimeType() << QStringLiteral("application/x-kolab-objects"))
+    , cCollectionOnlyContentMimeTypes(QStringList() << Akonadi::Collection::mimeType())
+{
+    mRequestedMetadata << "/shared/vendor/kolab/folder-type";
+    mRequestedMetadata << "/private/vendor/kolab/folder-type";
+}
+
+KolabRetrieveCollectionsTask::~KolabRetrieveCollectionsTask()
+{
+
+}
+
+void KolabRetrieveCollectionsTask::doStart(KIMAP::Session *session)
+{
+    Trace();
+    qCDebug(KOLABRESOURCE_LOG) << "Starting collection retrieval";
+    mTime.start();
+    mSession = session;
+
+    Akonadi::Collection root;
+    root.setName(resourceName());
+    root.setRemoteId(rootRemoteId());
+    root.setContentMimeTypes(QStringList(Akonadi::Collection::mimeType()));
+    root.setParentCollection(Akonadi::Collection::root());
+    root.addAttribute(new NoSelectAttribute(true));
+    root.attribute<Akonadi::EntityDisplayAttribute>(Akonadi::Collection::AddIfMissing)->setIconName(QStringLiteral("kolab"));
+
+    Akonadi::CachePolicy policy;
+    policy.setInheritFromParent(false);
+    policy.setSyncOnDemand(true);
+
+    QStringList localParts;
+    localParts << QLatin1String(Akonadi::MessagePart::Envelope)
+               << QLatin1String(Akonadi::MessagePart::Header);
+    int cacheTimeout = 60;
+
+    if (isDisconnectedModeEnabled()) {
+        // For disconnected mode we also cache the body
+        // and we keep all data indifinitely
+        localParts << QLatin1String(Akonadi::MessagePart::Body);
+        cacheTimeout = -1;
+    }
+
+    policy.setLocalParts(localParts);
+    policy.setCacheTimeout(cacheTimeout);
+    policy.setIntervalCheckTime(intervalCheckTime());
+
+    root.setCachePolicy(policy);
+
+    mMailCollections.insert(QString(), root);
+
+    Trace() << "subscription enabled: " << isSubscriptionEnabled();
+    //jobs are serialized by the session
+    if (isSubscriptionEnabled()) {
+        KIMAP::ListJob *fullListJob = new KIMAP::ListJob(session);
+        fullListJob->setOption(KIMAP::ListJob::NoOption);
+        fullListJob->setQueriedNamespaces(serverNamespaces());
+        connect(fullListJob, &KIMAP::ListJob::mailBoxesReceived,
+                this, &KolabRetrieveCollectionsTask::onFullMailBoxesReceived);
+        connect(fullListJob, &KJob::result, this, &KolabRetrieveCollectionsTask::onFullMailBoxesReceiveDone);
+        mJobs++;
+        fullListJob->start();
+    }
+
+    KIMAP::ListJob *listJob = new KIMAP::ListJob(session);
+    listJob->setOption(KIMAP::ListJob::IncludeUnsubscribed);
+    listJob->setQueriedNamespaces(serverNamespaces());
+    connect(listJob, &KIMAP::ListJob::mailBoxesReceived,
+            this, &KolabRetrieveCollectionsTask::onMailBoxesReceived);
+    connect(listJob, &KJob::result, this, &KolabRetrieveCollectionsTask::onMailBoxesReceiveDone);
+    mJobs++;
+    listJob->start();
+}
+
+void KolabRetrieveCollectionsTask::onMailBoxesReceived(const QList< KIMAP::MailBoxDescriptor > &descriptors,
+        const QList< QList<QByteArray> > &flags)
+{
+    for (int i = 0; i < descriptors.size(); ++i) {
+        const KIMAP::MailBoxDescriptor descriptor = descriptors[i];
+        createCollection(descriptor.name, flags.at(i), !isSubscriptionEnabled() || mSubscribedMailboxes.contains(descriptor.name));
+    }
+    checkDone();
+}
+
+Akonadi::Collection KolabRetrieveCollectionsTask::getOrCreateParent(const QString &path)
+{
+    if (mMailCollections.contains(path)) {
+        return mMailCollections.value(path);
+    }
+    //create a dummy collection
+    const QString separator = separatorCharacter();
+    const QStringList pathParts = path.split(separator);
+    const QString pathPart = pathParts.last();
+    Akonadi::Collection c;
+    c.setName(pathPart);
+    c.setRemoteId(separator + pathPart);
+    const QStringList parentPath = pathParts.mid(0, pathParts.size() - 1);
+    const Akonadi::Collection parentCollection = getOrCreateParent(parentPath.join(separator));
+    c.setParentCollection(parentCollection);
+
+    c.addAttribute(new NoSelectAttribute(true));
+    c.setContentMimeTypes(QStringList() << Akonadi::Collection::mimeType());
+    c.setRights(Akonadi::Collection::ReadOnly);
+    c.setEnabled(false);
+    setAttributes(c, pathParts, path);
+
+    mMailCollections.insert(path, c);
+    return c;
+}
+
+void KolabRetrieveCollectionsTask::setAttributes(Akonadi::Collection &c, const QStringList &pathParts, const QString &path)
+{
+
+    Akonadi::CollectionIdentificationAttribute *attr = c.attribute<Akonadi::CollectionIdentificationAttribute>(Akonadi::Collection::AddIfMissing);
+    attr->setIdentifier(path.toLatin1());
+
+    // If the folder is a other users folder block all alarms from default
+    if (isNamespaceFolder(path, resourceState()->userNamespaces())) {
+        Akonadi::BlockAlarmsAttribute *attr = c.attribute<Akonadi::BlockAlarmsAttribute>(Akonadi::Collection::AddIfMissing);
+        attr->blockEverything(true);
+    }
+
+    // If the folder is a other users top-level folder mark it accordingly
+    if (pathParts.size() == 1 && isNamespaceFolder(path, resourceState()->userNamespaces())) {
+        Akonadi::EntityDisplayAttribute *attr = c.attribute<Akonadi::EntityDisplayAttribute>(Akonadi::Collection::AddIfMissing);
+        attr->setDisplayName(i18n("Other Users"));
+        attr->setIconName(QStringLiteral("x-mail-distribution-list"));
+    }
+
+    //Mark user folders for searching
+    if (pathParts.size() >= 2 && isNamespaceFolder(path, resourceState()->userNamespaces())) {
+        Akonadi::CollectionIdentificationAttribute *attr = c.attribute<Akonadi::CollectionIdentificationAttribute>(Akonadi::Collection::AddIfMissing);
+        if (pathParts.size() == 2) {
+            attr->setCollectionNamespace("usertoplevel");
+        } else {
+            attr->setCollectionNamespace("user");
+        }
+    }
+
+    // If the folder is a shared folders top-level folder mark it accordingly
+    if (pathParts.size() == 1 && isNamespaceFolder(path, resourceState()->sharedNamespaces())) {
+        Akonadi::EntityDisplayAttribute *attr = c.attribute<Akonadi::EntityDisplayAttribute>(Akonadi::Collection::AddIfMissing);
+        attr->setDisplayName(i18n("Shared Folders"));
+        attr->setIconName(QStringLiteral("x-mail-distribution-list"));
+    }
+
+    //Mark shared folders for searching
+    if (pathParts.size() >= 2 && isNamespaceFolder(path, resourceState()->sharedNamespaces())) {
+        Akonadi::CollectionIdentificationAttribute *attr = c.attribute<Akonadi::CollectionIdentificationAttribute>(Akonadi::Collection::AddIfMissing);
+        attr->setCollectionNamespace("shared");
+    }
+
+}
+
+void KolabRetrieveCollectionsTask::createCollection(const QString &mailbox, const QList<QByteArray> &currentFlags, bool isSubscribed)
+{
+    const QString separator = separatorCharacter();
+    Q_ASSERT(separator.size() == 1);
+    const QString boxName = mailbox.endsWith(separator)
+                            ? mailbox.left(mailbox.size() - 1)
+                            : mailbox;
+    const QStringList pathParts = boxName.split(separator);
+    const QString pathPart = pathParts.last();
+
+    Akonadi::Collection c;
+    //If we had a dummy collection we need to replace it
+    if (mMailCollections.contains(mailbox)) {
+        c = mMailCollections.value(mailbox);
+    }
+    c.setName(pathPart);
+    c.setRemoteId(separator + pathPart);
+    const QStringList parentPath = pathParts.mid(0, pathParts.size() - 1);
+    const Akonadi::Collection parentCollection = getOrCreateParent(parentPath.join(separator));
+    c.setParentCollection(parentCollection);
+    //TODO get from ResourceState, and add KMime::Message::mimeType() for the normal imap resource by default
+    //We add a dummy mimetype, otherwise the itemsync doesn't even work (action is disabled and resourcebase aborts the operation)
+    c.setContentMimeTypes(cDefaultMimeTypes);
+    c.setKeepLocalChanges(cDefaultKeepLocalChanges);
+
+    //assume LRS, until myrights is executed
+    if (serverCapabilities().contains(QStringLiteral("ACL"))) {
+        c.setRights(Akonadi::Collection::ReadOnly);
+    } else {
+        c.setRights(Akonadi::Collection::AllRights);
+    }
+
+    setAttributes(c, pathParts, mailbox);
+
+    // If the folder is the Inbox, make some special settings.
+    if (pathParts.size() == 1 && pathPart.compare(QLatin1String("inbox"), Qt::CaseInsensitive) == 0) {
+        Akonadi::EntityDisplayAttribute *attr = c.attribute<Akonadi::EntityDisplayAttribute>(Akonadi::Collection::AddIfMissing);
+        attr->setDisplayName(i18n("Inbox"));
+        attr->setIconName(QStringLiteral("mail-folder-inbox"));
+        setIdleCollection(c);
+    }
+
+    // If this folder is a noselect folder, make some special settings.
+    if (currentFlags.contains("\\noselect")) {
+        c.addAttribute(new NoSelectAttribute(true));
+        c.setContentMimeTypes(cCollectionOnlyContentMimeTypes);
+        c.setRights(Akonadi::Collection::ReadOnly);
+    } else {
+        // remove the noselect attribute explicitly, in case we had set it before (eg. for non-subscribed non-leaf folders)
+        c.removeAttribute<NoSelectAttribute>();
+    }
+
+    // If this folder is a noinferiors folder, it is not allowed to create subfolders inside.
+    if (currentFlags.contains("\\noinferiors")) {
+        //qCDebug(KOLABRESOURCE_LOG) << "Noinferiors: " << currentPath;
+        c.addAttribute(new NoInferiorsAttribute(true));
+        c.setRights(c.rights() & ~Akonadi::Collection::CanCreateCollection);
+    }
+    c.setEnabled(isSubscribed);
+
+    // qCDebug(KOLABRESOURCE_LOG) << "creating collection " << mailbox << " with parent " << parentPath;
+    mMailCollections.insert(mailbox, c);
+}
+
+void KolabRetrieveCollectionsTask::onMailBoxesReceiveDone(KJob *job)
+{
+    Trace();
+    qCDebug(KOLABRESOURCE_LOG) << "All mailboxes received: " << mTime.elapsed();
+    qCDebug(KOLABRESOURCE_LOG) << "in total: " << mMailCollections.size();
+    mJobs--;
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << QStringLiteral("Failed to retrieve mailboxes: ") + job->errorString();
+        cancelTask(QStringLiteral("Collection retrieval failed"));
+    } else {
+        QSet<QString> mailboxes;
+        Q_FOREACH (const QString &mailbox, mMailCollections.keys()) {
+            if (!mailbox.isEmpty() && !isNamespaceFolder(mailbox, resourceState()->userNamespaces() + resourceState()->sharedNamespaces())) {
+                mailboxes << mailbox;
+            }
+        }
+
+        //Only request metadata for subscribed Other Users Folders
+        const QStringList metadataMailboxes = mailboxes.unite(mSubscribedMailboxes.toList().toSet()).toList();
+
+        RetrieveMetadataJob *metadata = new RetrieveMetadataJob(mSession, metadataMailboxes, serverCapabilities(), mRequestedMetadata, separatorCharacter(), resourceState()->sharedNamespaces(), resourceState()->userNamespaces(), this);
+        connect(metadata, &KJob::result, this, &KolabRetrieveCollectionsTask::onMetadataRetrieved);
+        mJobs++;
+        metadata->start();
+    }
+}
+
+void KolabRetrieveCollectionsTask::applyRights(QHash<QString, KIMAP::Acl::Rights> rights)
+{
+    // qCDebug(KOLABRESOURCE_LOG) << rights;
+    Q_FOREACH (const QString &mailbox, rights.keys()) {
+        if (mMailCollections.contains(mailbox)) {
+            const KIMAP::Acl::Rights imapRights = rights.value(mailbox);
+            QStringList parts = mailbox.split(separatorCharacter());
+            parts.removeLast();
+            QString parentMailbox = parts.join(separatorCharacter());
+
+            KIMAP::Acl::Rights parentImapRights;
+            //If the parent folder is not existing we cant rename
+            if (!parentMailbox.isEmpty() && rights.contains(parentMailbox)) {
+                parentImapRights = rights.value(parentMailbox);
+            }
+            // qCDebug(KOLABRESOURCE_LOG) << mailbox << parentMailbox << imapRights << parentImapRights;
+
+            Akonadi::Collection &collection = mMailCollections[mailbox];
+            CollectionMetadataHelper::applyRights(collection, imapRights, parentImapRights);
+
+            // Store the mailbox ACLs
+            Akonadi::ImapAclAttribute *aclAttribute = collection.attribute<Akonadi::ImapAclAttribute>(Akonadi::Collection::AddIfMissing);
+            const KIMAP::Acl::Rights oldRights = aclAttribute->myRights();
+            if (oldRights != imapRights) {
+                aclAttribute->setMyRights(imapRights);
+            }
+        } else {
+            qCWarning(KOLABRESOURCE_LOG) << "Can't find mailbox " << mailbox;
+        }
+    }
+}
+
+void KolabRetrieveCollectionsTask::applyMetadata(QHash<QString, QMap<QByteArray, QByteArray> > metadataMap)
+{
+    // qCDebug(KOLABRESOURCE_LOG) << metadataMap;
+    Q_FOREACH (const QString &mailbox, metadataMap.keys()) {
+        const QMap<QByteArray, QByteArray> metadata  = metadataMap.value(mailbox);
+        if (mMailCollections.contains(mailbox)) {
+            Akonadi::Collection &collection = mMailCollections[mailbox];
+            // qCDebug(KOLABRESOURCE_LOG) << "setting metadata: " << mailbox << metadata;
+            collection.attribute<Akonadi::CollectionAnnotationsAttribute>(Akonadi::Collection::AddIfMissing)->setAnnotations(metadata);
+            const QByteArray type = KolabHelpers::getFolderTypeAnnotation(metadata);
+            const Kolab::FolderType folderType = KolabHelpers::folderTypeFromString(type);
+            collection.setContentMimeTypes(KolabHelpers::getContentMimeTypes(folderType));
+            QSet<QByteArray> keepLocalChanges = collection.keepLocalChanges();
+            keepLocalChanges.remove(cContentMimeTypes);
+            collection.setKeepLocalChanges(keepLocalChanges);
+        }
+    }
+}
+
+void KolabRetrieveCollectionsTask::onMetadataRetrieved(KJob *job)
+{
+    Trace();
+    qCDebug(KOLABRESOURCE_LOG) << mTime.elapsed();
+    mJobs--;
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << "Error while retrieving metadata, aborting collection retrieval: " << job->errorString();
+        cancelTask(QStringLiteral("Collection retrieval failed"));
+    } else {
+        RetrieveMetadataJob *metadata = static_cast<RetrieveMetadataJob *>(job);
+        applyRights(metadata->mRights);
+        applyMetadata(metadata->mMetadata);
+        checkDone();
+    }
+}
+
+void KolabRetrieveCollectionsTask::checkDone()
+{
+    if (!mJobs) {
+        Trace() << "done " << mMailCollections.size();
+        collectionsRetrieved(Akonadi::valuesToVector(mMailCollections));
+        qCDebug(KOLABRESOURCE_LOG) << "done " <<  mTime.elapsed();
+    }
+}
+
+void KolabRetrieveCollectionsTask::onFullMailBoxesReceived(const QList< KIMAP::MailBoxDescriptor > &descriptors,
+        const QList< QList< QByteArray > > &flags)
+{
+    Q_UNUSED(flags);
+    foreach (const KIMAP::MailBoxDescriptor &descriptor, descriptors) {
+        mSubscribedMailboxes.insert(descriptor.name);
+    }
+}
+
+void KolabRetrieveCollectionsTask::onFullMailBoxesReceiveDone(KJob *job)
+{
+    Trace();
+    qCDebug(KOLABRESOURCE_LOG) << "received subscribed collections " <<  mTime.elapsed();
+    mJobs--;
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << QStringLiteral("Failed to retrieve subscribed collections: ") + job->errorString();
+        cancelTask(QStringLiteral("Collection retrieval failed"));
+    } else {
+        checkDone();
+    }
+}
diff --git a/resources/kolab/kolabretrievecollectionstask.h b/resources/kolab/kolabretrievecollectionstask.h
new file mode 100644 (file)
index 0000000..61d6271
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+    Copyright (c) 2010 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Ottens <kevin@kdab.com>
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef KOLABRETRIEVECOLLECTIONSTASK_H
+#define KOLABRETRIEVECOLLECTIONSTASK_H
+
+#include <resourcetask.h>
+
+#include <AkonadiCore/Collection>
+
+#include <kimap/listjob.h>
+
+class KolabRetrieveCollectionsTask : public ResourceTask
+{
+    Q_OBJECT
+
+public:
+    explicit KolabRetrieveCollectionsTask(ResourceStateInterface::Ptr resource, QObject *parent = Q_NULLPTR);
+    virtual ~KolabRetrieveCollectionsTask();
+
+private slots:
+    void onMailBoxesReceived(const QList<KIMAP::MailBoxDescriptor> &descriptors,
+                             const QList< QList<QByteArray> > &flags);
+    void onMailBoxesReceiveDone(KJob *job);
+    void onFullMailBoxesReceived(const QList<KIMAP::MailBoxDescriptor> &descriptors, const QList<QList<QByteArray> > &flags);
+    void onFullMailBoxesReceiveDone(KJob *job);
+    void onMetadataRetrieved(KJob *job);
+
+protected:
+    virtual void doStart(KIMAP::Session *session);
+
+private:
+    void checkDone();
+    Akonadi::Collection getOrCreateParent(const QString &parentPath);
+    void createCollection(const QString &mailbox, const QList<QByteArray> &flags, bool isSubscribed);
+    void setAttributes(Akonadi::Collection &c, const QStringList &pathParts, const QString &path);
+    void applyRights(QHash<QString, KIMAP::Acl::Rights> rights);
+    void applyMetadata(QHash<QString, QMap<QByteArray, QByteArray> > metadata);
+
+    int mJobs;
+    QHash<QString, Akonadi::Collection> mMailCollections;
+    QSet<QString> mSubscribedMailboxes;
+    QSet<QByteArray> mRequestedMetadata;
+    KIMAP::Session *mSession;
+    QTime mTime;
+    //For implicit sharing
+    const QByteArray cContentMimeTypes;
+    const QByteArray cAccessRights;
+    const QByteArray cImapAcl;
+    const QByteArray cCollectionAnnotations;
+    const QSet<QByteArray> cDefaultKeepLocalChanges;
+    const QStringList cDefaultMimeTypes;
+    const QStringList cCollectionOnlyContentMimeTypes;
+};
+
+class RetrieveMetadataJob : public KJob
+{
+    Q_OBJECT
+public:
+    RetrieveMetadataJob(KIMAP::Session *session, const QStringList &mailboxes, const QStringList &serverCapabilities, const QSet<QByteArray> &requestedMetadata, const QString &separator, const QList <KIMAP::MailBoxDescriptor > &sharedNamespace, const QList <KIMAP::MailBoxDescriptor > &userNamespace, QObject *parent = Q_NULLPTR);
+    void start();
+
+    QHash<QString, QMap<QByteArray, QByteArray> > mMetadata;
+    QHash<QString, KIMAP::Acl::Rights> mRights;
+
+private:
+    void checkDone();
+    int mJobs;
+    QSet<QByteArray> mRequestedMetadata;
+    QStringList mServerCapabilities;
+    QStringList mMailboxes;
+    KIMAP::Session *mSession;
+    QString mSeparator;
+    QList <KIMAP::MailBoxDescriptor > mSharedNamespace;
+    QList <KIMAP::MailBoxDescriptor > mUserNamespace;
+
+private Q_SLOTS:
+    void onGetMetaDataDone(KJob *job);
+    void onRightsReceived(KJob *job);
+};
+
+#endif
diff --git a/resources/kolab/kolabretrievetagstask.cpp b/resources/kolab/kolabretrievetagstask.cpp
new file mode 100644 (file)
index 0000000..ddc46c8
--- /dev/null
@@ -0,0 +1,227 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "kolabretrievetagstask.h"
+#include "kolabresource_debug.h"
+#include "tagchangehelper.h"
+
+#include <kimap/selectjob.h>
+#include <kimap/fetchjob.h>
+#include <imapflags.h>
+#include <kolabobject.h>
+#include "tracer.h"
+
+KolabRetrieveTagTask::KolabRetrieveTagTask(ResourceStateInterface::Ptr resource, RetrieveType type, QObject *parent)
+    : KolabRelationResourceTask(resource, parent)
+    , mSession(0)
+    , mRetrieveType(type)
+{
+}
+
+void KolabRetrieveTagTask::startRelationTask(KIMAP::Session *session)
+{
+    mSession = session;
+    const QString mailBox = mailBoxForCollection(relationCollection());
+
+    KIMAP::SelectJob *select = new KIMAP::SelectJob(session);
+    select->setMailBox(mailBox);
+    connect(select, &KJob::result,
+            this, &KolabRetrieveTagTask::onFinalSelectDone);
+    select->start();
+}
+
+void KolabRetrieveTagTask::onFinalSelectDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << job->errorString();
+        cancelTask(job->errorString());
+        return;
+    }
+
+    KIMAP::SelectJob *select = static_cast<KIMAP::SelectJob *>(job);
+    KIMAP::FetchJob *fetch = new KIMAP::FetchJob(select->session());
+
+    if (select->messageCount() == 0) {
+        taskComplete();
+        return;
+    }
+
+    KIMAP::ImapSet set;
+    set.add(KIMAP::ImapInterval(1, 0));
+    fetch->setSequenceSet(set);
+    fetch->setUidBased(false);
+
+    KIMAP::FetchJob::FetchScope scope;
+    scope.parts.clear();
+    scope.mode = KIMAP::FetchJob::FetchScope::Full;
+    fetch->setScope(scope);
+
+    connect(fetch, SIGNAL(headersReceived(QString,
+                                          QMap<qint64, qint64>,
+                                          QMap<qint64, qint64>,
+                                          QMap<qint64, KIMAP::MessageAttribute>,
+                                          QMap<qint64, KIMAP::MessageFlags>,
+                                          QMap<qint64, KIMAP::MessagePtr>)),
+            this, SLOT(onHeadersReceived(QString,
+                                         QMap<qint64, qint64>,
+                                         QMap<qint64, qint64>,
+                                         QMap<qint64, KIMAP::MessageAttribute>,
+                                         QMap<qint64, KIMAP::MessageFlags>,
+                                         QMap<qint64, KIMAP::MessagePtr>)));
+    connect(fetch, &KJob::result,
+            this, &KolabRetrieveTagTask::onHeadersFetchDone);
+    fetch->start();
+}
+
+void KolabRetrieveTagTask::onHeadersReceived(const QString &mailBox,
+        const QMap<qint64, qint64> &uids,
+        const QMap<qint64, qint64> &sizes,
+        const QMap<qint64, KIMAP::MessageAttribute> &attrs,
+        const QMap<qint64, KIMAP::MessageFlags> &flags,
+        const QMap<qint64, KIMAP::MessagePtr> &messages)
+{
+    KIMAP::FetchJob *fetch = static_cast<KIMAP::FetchJob *>(sender());
+    Q_ASSERT(fetch);
+
+    foreach (qint64 number, uids.keys()) { //krazy:exclude=foreach
+        if (flags[number].contains(ImapFlags::Deleted)) {
+            continue;
+        }
+        const KMime::Message::Ptr msg = messages[number];
+        const Kolab::KolabObjectReader reader(msg);
+        switch (reader.getType()) {
+        case Kolab::RelationConfigurationObject:
+            if (mRetrieveType == RetrieveTags && reader.isTag()) {
+                extractTag(reader, uids[number]);
+            } else if (mRetrieveType == RetrieveRelations && reader.isRelation()) {
+                extractRelation(reader, uids[number]);
+            }
+            break;
+
+        default:
+            break;
+        }
+    }
+}
+
+Akonadi::Item KolabRetrieveTagTask::extractMember(const Kolab::RelationMember &member)
+{
+    //TODO should we create a dummy item if it isn't yet available?
+    Akonadi::Item i;
+    if (!member.gid.isEmpty()) {
+        //Reference by GID
+        i.setGid(member.gid);
+    } else {
+        //Reference by imap uid
+        if (member.uid < 0) {
+            return Akonadi::Item();
+        }
+        i.setRemoteId(QString::number(member.uid));
+        qCDebug(KOLABRESOURCE_LOG) << "got member: " << member.uid << member.mailbox;
+        Akonadi::Collection parent;
+        {
+            //The root collection is not part of the mailbox path
+            Akonadi::Collection col;
+            col.setRemoteId(rootRemoteId());
+            col.setParentCollection(Akonadi::Collection::root());
+            parent = col;
+        }
+        Q_FOREACH (const QByteArray part, member.mailbox) {
+            Akonadi::Collection col;
+            col.setRemoteId(separatorCharacter() + QString::fromLatin1(part));
+            col.setParentCollection(parent);
+            parent = col;
+        }
+        i.setParentCollection(parent);
+    }
+    return i;
+}
+
+void KolabRetrieveTagTask::extractTag(const Kolab::KolabObjectReader &reader, qint64 remoteUid)
+{
+    Akonadi::Tag tag = reader.getTag();
+    tag.setRemoteId(QByteArray::number(remoteUid));
+    mTags << tag;
+
+    Trace() << "Extracted tag: " << tag.gid() << " remoteId: " << remoteUid << tag.remoteId();
+
+    Akonadi::Item::List members;
+    Q_FOREACH (const QString &memberUrl, reader.getTagMembers()) {
+        Kolab::RelationMember member = Kolab::parseMemberUrl(memberUrl);
+        const Akonadi::Item i = extractMember(member);
+        //TODO implement fallback to search if uid is not available
+        if (!i.remoteId().isEmpty() || !i.gid().isEmpty()) {
+            members << i;
+        } else {
+            qCWarning(KOLABRESOURCE_LOG) << "Failed to parse member: " << memberUrl;
+        }
+    }
+    mTagMembers.insert(QString::fromLatin1(tag.remoteId()), members);
+}
+
+void KolabRetrieveTagTask::extractRelation(const Kolab::KolabObjectReader &reader, qint64 remoteUid)
+{
+    Akonadi::Item::List members;
+    Q_FOREACH (const QString &memberUrl, reader.getTagMembers()) {
+        Kolab::RelationMember member = Kolab::parseMemberUrl(memberUrl);
+        const Akonadi::Item i = extractMember(member);
+        //TODO implement fallback to search if uid is not available
+        if (!i.remoteId().isEmpty() || !i.gid().isEmpty()) {
+            members << i;
+        } else {
+            qCWarning(KOLABRESOURCE_LOG) << "Failed to parse member: " << memberUrl;
+        }
+    }
+    if (members.size() != 2) {
+        qCWarning(KOLABRESOURCE_LOG) << "Wrong numbers of members for a relation, expected 2: " << members.size();
+        return;
+    }
+
+    Akonadi::Relation relation = reader.getRelation();
+    relation.setType(Akonadi::Relation::GENERIC);
+    relation.setRemoteId(QByteArray::number(remoteUid));
+    relation.setLeft(members.at(0));
+    relation.setRight(members.at(1));
+    mRelations << relation;
+}
+
+void KolabRetrieveTagTask::onHeadersFetchDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << "Fetch job failed " << job->errorString();
+        cancelTask(job->errorString());
+        return;
+    }
+
+    taskComplete();
+}
+
+void KolabRetrieveTagTask::taskComplete()
+{
+    if (mRetrieveType == RetrieveTags) {
+        qCDebug(KOLABRESOURCE_LOG) << "Fetched tags: " << mTags.size() << mTagMembers.size();
+        resourceState()->tagsRetrieved(mTags, mTagMembers);
+    } else if (mRetrieveType == RetrieveRelations) {
+        qCDebug(KOLABRESOURCE_LOG) << "Fetched relations:" << mRelations.size();
+        resourceState()->relationsRetrieved(mRelations);
+    }
+
+    deleteLater();
+}
+
diff --git a/resources/kolab/kolabretrievetagstask.h b/resources/kolab/kolabretrievetagstask.h
new file mode 100644 (file)
index 0000000..96ae429
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef KOLABRETRIEVETAGSTASK_H
+#define KOLABRETRIEVETAGSTASK_H
+
+#include "kolabrelationresourcetask.h"
+#include <tag.h>
+
+namespace Kolab
+{
+class KolabObjectReader;
+class RelationMember;
+} // namespace Kolab
+
+class KolabRetrieveTagTask : public KolabRelationResourceTask
+{
+    Q_OBJECT
+public:
+    enum RetrieveType {
+        RetrieveTags,
+        RetrieveRelations
+    };
+
+    explicit KolabRetrieveTagTask(ResourceStateInterface::Ptr resource, RetrieveType type, QObject *parent = Q_NULLPTR);
+
+protected:
+    virtual void startRelationTask(KIMAP::Session *session);
+
+private:
+    KIMAP::Session *mSession;
+    Akonadi::Tag::List mTags;
+    QHash<QString, Akonadi::Item::List> mTagMembers;
+    Akonadi::Relation::List mRelations;
+    RetrieveType mRetrieveType;
+
+private Q_SLOTS:
+    void onFinalSelectDone(KJob *job);
+    void onHeadersReceived(const QString &mailBox,
+                           const QMap<qint64, qint64> &uids,
+                           const QMap<qint64, qint64> &sizes,
+                           const QMap<qint64, KIMAP::MessageAttribute> &attrs,
+                           const QMap<qint64, KIMAP::MessageFlags> &flags,
+                           const QMap<qint64, KIMAP::MessagePtr> &messages);
+    void onHeadersFetchDone(KJob *job);
+
+private:
+    void extractTag(const Kolab::KolabObjectReader &reader, qint64 remoteUid);
+    void extractRelation(const Kolab::KolabObjectReader &reader, qint64 remoteUid);
+    Akonadi::Item extractMember(const Kolab::RelationMember &member);
+    void taskComplete();
+};
+
+#endif // KOLABCHANGETAGTASK_H
diff --git a/resources/kolab/kolabsettings.cpp b/resources/kolab/kolabsettings.cpp
new file mode 100644 (file)
index 0000000..ef7f4fd
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+    Copyright (c) 2014 Sandro Knauß <knauss@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "kolabsettings.h"
+
+KolabSettings::KolabSettings(WId winId) : Settings(winId)
+{
+    changeDefaults();
+    load();
+}
+
+void KolabSettings::changeDefaults()
+{
+    setCurrentGroup(QStringLiteral("network"));
+    KConfigSkeleton::ItemInt  *itemImapPort;
+    itemImapPort = (KConfigSkeleton::ItemInt *) findItem(QStringLiteral("ImapPort"));
+    itemImapPort->setDefaultValue(143);
+    KConfigSkeleton::ItemString  *itemSafety;
+    itemSafety = (KConfigSkeleton::ItemString *) findItem(QStringLiteral("Safety"));
+    itemSafety->setDefaultValue(QStringLiteral("STARTTLS"));
+    KConfigSkeleton::ItemBool  *itemSubscriptionEnabled;
+    itemSubscriptionEnabled = (KConfigSkeleton::ItemBool *) findItem(QStringLiteral("SubscriptionEnabled"));
+    itemSubscriptionEnabled->setDefaultValue(true);
+
+    setCurrentGroup(QStringLiteral("cache"));
+    KConfigSkeleton::ItemBool  *itemDisconnectedModeEnabled;
+    itemDisconnectedModeEnabled = (KConfigSkeleton::ItemBool *) findItem(QStringLiteral("DisconnectedModeEnabled"));
+    itemDisconnectedModeEnabled->setDefaultValue(true);
+
+    setCurrentGroup(QStringLiteral("siever"));
+    KConfigSkeleton::ItemBool  *itemSieveSupport;
+    itemSieveSupport = (KConfigSkeleton::ItemBool *) findItem(QStringLiteral("SieveSupport"));
+    itemSieveSupport->setDefaultValue(true);
+    KConfigSkeleton::ItemBool  *itemSieveReuseConfig;
+    itemSieveReuseConfig = (KConfigSkeleton::ItemBool *) findItem(QStringLiteral("SieveReuseConfig"));
+    itemSieveReuseConfig->setDefaultValue(true);
+}
diff --git a/resources/kolab/kolabsettings.h b/resources/kolab/kolabsettings.h
new file mode 100644 (file)
index 0000000..75b0a8f
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+    Copyright (c) 2014 Sandro Knauß <knauss@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef KOLABSETTINGS_H
+#define KOLABSETTINGS_H
+
+#include "settings.h"
+
+class KolabSettings : public Settings
+{
+    Q_OBJECT
+public:
+    explicit KolabSettings(WId = 0);
+
+protected:
+    virtual void changeDefaults();
+};
+
+#endif
diff --git a/resources/kolab/tagchangehelper.cpp b/resources/kolab/tagchangehelper.cpp
new file mode 100644 (file)
index 0000000..a0120ae
--- /dev/null
@@ -0,0 +1,129 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Krammer <kevin.krammer@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "tagchangehelper.h"
+#include "kolabresource_debug.h"
+#include "kolabrelationresourcetask.h"
+#include "kolabhelpers.h"
+#include "updatemessagejob.h"
+
+#include <imapflags.h>
+#include <uidnextattribute.h>
+
+#include <kolabobject.h>
+
+#include <kimap/appendjob.h>
+#include <kimap/searchjob.h>
+#include <kimap/selectjob.h>
+#include <kimap/session.h>
+#include <kimap/storejob.h>
+#include <replacemessagejob.h>
+#include <AkonadiCore/TagModifyJob>
+
+#include <KDE/KLocalizedString>
+
+KMime::Message::Ptr TagConverter::createMessage(const Akonadi::Tag &tag, const Akonadi::Item::List &items, const QString &username)
+{
+    QStringList itemRemoteIds;
+    itemRemoteIds.reserve(items.count());
+    Q_FOREACH (const Akonadi::Item &item, items) {
+        const QString memberUrl = KolabHelpers::createMemberUrl(item, username);
+        if (!memberUrl.isEmpty()) {
+            itemRemoteIds << memberUrl;
+        }
+    }
+
+    // save message to the server.
+    const QLatin1String productId("Akonadi-Kolab-Resource");
+    const KMime::Message::Ptr message = Kolab::KolabObjectWriter::writeTag(tag, itemRemoteIds, Kolab::KolabV3, productId);
+    return message;
+}
+
+struct TagMerger : public Merger {
+    virtual ~TagMerger() {}
+    virtual KMime::Message::Ptr merge(KMime::Message::Ptr newMessage, QList<KMime::Message::Ptr> conflictingMessages) const
+    {
+        qCDebug(KOLABRESOURCE_LOG) << "Got " << conflictingMessages.size() << " conflicting relation configuration objects. Overwriting with local version.";
+        return newMessage;
+    }
+};
+
+TagChangeHelper::TagChangeHelper(KolabRelationResourceTask *parent)
+    : QObject(parent)
+    , mTask(parent)
+{
+}
+
+void TagChangeHelper::start(const Akonadi::Tag &tag, const KMime::Message::Ptr &message, KIMAP::Session *session)
+{
+    Q_ASSERT(tag.isValid());
+    const QString mailBox = mTask->mailBoxForCollection(mTask->relationCollection());
+    const qint64 oldUid = tag.remoteId().toLongLong();
+    qCDebug(KOLABRESOURCE_LOG) << mailBox << oldUid;
+
+    const qint64 uidNext = -1;
+
+    UpdateMessageJob *append = new UpdateMessageJob(message, session, tag.gid(), QSharedPointer<TagMerger>(new TagMerger), mailBox, uidNext, oldUid, this);
+    connect(append, &KJob::result, this, &TagChangeHelper::onReplaceDone);
+    append->setProperty("tag", QVariant::fromValue(tag));
+    append->start();
+}
+
+void TagChangeHelper::recordNewUid(qint64 newUid, Akonadi::Tag tag)
+{
+    Q_ASSERT(newUid > 0);
+    Q_ASSERT(tag.isValid());
+
+    const QByteArray remoteId =  QByteArray::number(newUid);
+    qCDebug(KOLABRESOURCE_LOG) << "Setting remote ID to " << remoteId << " on tag with local id: " << tag.id();
+    //Make sure we only update the id and send nothing else
+    Akonadi::Tag updateTag;
+    updateTag.setId(tag.id());
+    updateTag.setRemoteId(remoteId);
+    Akonadi::TagModifyJob *modJob = new Akonadi::TagModifyJob(updateTag);
+    connect(modJob, &KJob::result, this, &TagChangeHelper::onModifyDone);
+}
+
+void TagChangeHelper::onReplaceDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << "Replace failed: " << job->errorString();
+    }
+    UpdateMessageJob *replaceJob = static_cast<UpdateMessageJob *>(job);
+    const qint64 newUid = replaceJob->newUid();
+    const Akonadi::Tag tag = job->property("tag").value<Akonadi::Tag>();
+    if (newUid > 0) {
+        recordNewUid(newUid, tag);
+    } else {
+        emit cancelTask(job->errorString());
+    }
+}
+
+void TagChangeHelper::onModifyDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << "Modify failed: " << job->errorString();
+        emit cancelTask(job->errorString());
+        return;
+    }
+    emit changeCommitted();
+}
+
diff --git a/resources/kolab/tagchangehelper.h b/resources/kolab/tagchangehelper.h
new file mode 100644 (file)
index 0000000..7393f04
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+    Copyright (c) 2014 Klarälvdalens Datakonsult AB,
+                       a KDAB Group company <info@kdab.com>
+    Author: Kevin Krammer <kevin.krammer@kdab.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef TAGCHANGEHELPER_H
+#define TAGCHANGEHELPER_H
+
+#include <collection.h>
+#include <item.h>
+#include <kmime/kmime_message.h>
+
+#include <QObject>
+
+namespace KIMAP
+{
+class Session;
+}
+
+namespace Akonadi
+{
+class Tag;
+}
+
+class KolabRelationResourceTask;
+
+struct TagConverter {
+    KMime::Message::Ptr createMessage(const Akonadi::Tag &tag, const Akonadi::Item::List &items, const QString &username);
+};
+
+class TagChangeHelper : public QObject
+{
+    Q_OBJECT
+public:
+    explicit TagChangeHelper(KolabRelationResourceTask *parent = 0);
+
+    void start(const Akonadi::Tag &tag, const KMime::Message::Ptr &message, KIMAP::Session *session);
+
+Q_SIGNALS:
+    void applyCollectionChanges(const Akonadi::Collection &collection);
+    void cancelTask(const QString &errorText);
+    void changeCommitted();
+
+private:
+    KolabRelationResourceTask *const mTask;
+
+private:
+    void recordNewUid(qint64 newUid, Akonadi::Tag tag);
+
+private Q_SLOTS:
+    void onReplaceDone(KJob *job);
+    void onModifyDone(KJob *job);
+};
+
+#endif // TAGCHANGEHELPER_H
diff --git a/resources/kolab/tests/CMakeLists.txt b/resources/kolab/tests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..a17ab9b
--- /dev/null
@@ -0,0 +1,25 @@
+set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR})
+
+# if kdepimlibs was built without -DKDE4_BUILD_TESTS, kimaptest doesn't exist.
+find_path(KIMAPTEST_INCLUDE_DIR NAMES kimaptest/fakeserver.h)
+find_library(KIMAPTEST_LIBRARY NAMES kimaptest)
+
+set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR})
+set(KDEPIMLIBS_RUN_ISOLATED_TESTS TRUE)
+set(KDEPIMLIBS_RUN_SQLITE_ISOLATED_TESTS TRUE)
+
+if(KIMAPTEST_INCLUDE_DIR AND KIMAPTEST_LIBRARY)
+    MACRO(KOLAB_RESOURCE_ISOLATED_TESTS)
+        FOREACH(_testname ${ARGN})
+            include_directories(${CMAKE_CURRENT_SOURCE_DIR}/.. ${CMAKE_CURRENT_BINARY_DIR}/.. ../../imap/autotests/)
+            add_akonadi_isolated_test_advanced(${_testname}.cpp "../../imap/autotests/dummypasswordrequester.cpp;../../imap/autotests/dummyresourcestate.cpp;../../imap/autotests/imaptestbase.cpp" "KF5::IMAP;${KIMAPTEST_LIBRARY};Qt5::Test;imapresource;kolabresource;akonaditest;${Libkolab_LIBRARIES}")
+            kde_enable_exceptions()
+        ENDFOREACH(_testname)
+    ENDMACRO(KOLAB_RESOURCE_ISOLATED_TESTS)
+
+    KOLAB_RESOURCE_ISOLATED_TESTS (
+        testretrievetagstask
+        testchangeitemstagstask
+    )
+endif()
+
diff --git a/resources/kolab/tests/imaptestbase.cpp b/resources/kolab/tests/imaptestbase.cpp
new file mode 100644 (file)
index 0000000..2659063
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "imaptestbase.h"
+
+ImapTestBase::ImapTestBase(QObject *parent)
+    : QObject(parent)
+{
+
+}
+
+QString ImapTestBase::defaultUserName() const
+{
+    return QLatin1String("test@kdab.com");
+}
+
+QString ImapTestBase::defaultPassword() const
+{
+    return QLatin1String("foobar");
+}
+
+ImapAccount *ImapTestBase::createDefaultAccount() const
+{
+    ImapAccount *account = new ImapAccount;
+
+    account->setServer(QLatin1String("127.0.0.1"));
+    account->setPort(5989);
+    account->setUserName(defaultUserName());
+    account->setSubscriptionEnabled(true);
+    account->setEncryptionMode(KIMAP::LoginJob::Unencrypted);
+    account->setAuthenticationMode(KIMAP::LoginJob::ClearText);
+
+    return account;
+}
+
+DummyPasswordRequester *ImapTestBase::createDefaultRequester()
+{
+    DummyPasswordRequester *requester = new DummyPasswordRequester(this);
+    requester->setPassword(defaultPassword());
+    return requester;
+}
+
+void ImapTestBase::setupTestCase()
+{
+    qRegisterMetaType<ImapAccount *>();
+    qRegisterMetaType<DummyPasswordRequester *>();
+    qRegisterMetaType<DummyResourceState::Ptr>();
+    qRegisterMetaType<KIMAP::Session *>();
+}
+
+QList<QByteArray> ImapTestBase::defaultAuthScenario() const
+{
+    QList<QByteArray> scenario;
+
+    scenario << FakeServer::greeting()
+             << "C: A000001 LOGIN \"test@kdab.com\" \"foobar\""
+             << "S: A000001 OK User Logged in";
+
+    return scenario;
+}
+
+QList<QByteArray> ImapTestBase::defaultPoolConnectionScenario(const QList<QByteArray> &customCapabilities) const
+{
+    QList<QByteArray> scenario;
+
+    QByteArray caps = "S: * CAPABILITY IMAP4 IMAP4rev1 UIDPLUS IDLE";
+    Q_FOREACH (const QByteArray &cap, customCapabilities) {
+        caps += " " + cap;
+    }
+
+    scenario << defaultAuthScenario()
+             << "C: A000002 CAPABILITY"
+             << caps
+             << "S: A000002 OK Completed";
+
+    return scenario;
+}
+
+bool ImapTestBase::waitForSignal(QObject *obj, const char *member, int timeout) const
+{
+    QEventLoop loop;
+    QTimer timer;
+
+    connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit()));
+
+    QSignalSpy spy(obj, member);
+    connect(obj, member, &loop, SLOT(quit()));
+
+    timer.setSingleShot(true);
+    timer.start(timeout);
+    loop.exec();
+    timer.stop();
+
+    return spy.count() == 1;
+}
+
+Akonadi::Collection ImapTestBase::createCollectionChain(const QString &remoteId) const
+{
+    QChar separator = remoteId.length() > 0 ? remoteId.at(0) : QLatin1Char('/');
+
+    Akonadi::Collection parent(1);
+    parent.setRemoteId(QLatin1String("root-id"));
+    parent.setParentCollection(Akonadi::Collection::root());
+    Akonadi::Entity::Id id = 2;
+
+    Akonadi::Collection collection = parent;
+
+    const QStringList collections = remoteId.split(separator, QString::SkipEmptyParts);
+    Q_FOREACH (const QString &colId, collections) {
+        collection = Akonadi::Collection(id);
+        collection.setRemoteId(separator + colId);
+        collection.setParentCollection(parent);
+
+        parent = collection;
+        id++;
+    }
+
+    return collection;
+}
+
diff --git a/resources/kolab/tests/imaptestbase.h b/resources/kolab/tests/imaptestbase.h
new file mode 100644 (file)
index 0000000..f36dd1c
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+   Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
+   Author: Kevin Ottens <kevin@kdab.com>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or ( at your option ) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#ifndef IMAPTESTBASE_H
+#define IMAPTESTBASE_H
+
+#include <qtest_kde.h>
+
+#include <kimaptest/fakeserver.h>
+
+#include "dummypasswordrequester.h"
+#include "dummyresourcestate.h"
+#include "imapaccount.h"
+#include "resourcetask.h"
+#include "sessionpool.h"
+
+Q_DECLARE_METATYPE(ImapAccount *)
+Q_DECLARE_METATYPE(DummyPasswordRequester *)
+Q_DECLARE_METATYPE(DummyResourceState::Ptr)
+Q_DECLARE_METATYPE(KIMAP::Session *)
+Q_DECLARE_METATYPE(QVariant)
+
+class ImapTestBase : public QObject
+{
+    Q_OBJECT
+
+public:
+    ImapTestBase(QObject *parent = 0);
+
+protected:
+    QString defaultUserName() const;
+    QString defaultPassword() const;
+    ImapAccount *createDefaultAccount() const;
+    DummyPasswordRequester *createDefaultRequester();
+    QList<QByteArray> defaultAuthScenario() const;
+    QList<QByteArray> defaultPoolConnectionScenario(const QList<QByteArray> &customCapabilities = QList<QByteArray>()) const;
+
+    bool waitForSignal(QObject *obj, const char *member, int timeout = 500) const;
+
+    Akonadi::Collection createCollectionChain(const QString &remoteId) const;
+
+private slots:
+    void setupTestCase();
+};
+
+#endif
diff --git a/resources/kolab/tests/testchangeitemstagstask.cpp b/resources/kolab/tests/testchangeitemstagstask.cpp
new file mode 100644 (file)
index 0000000..2e65daf
--- /dev/null
@@ -0,0 +1,235 @@
+/*
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+namespace Akonadi
+{
+class Tag;
+};
+
+unsigned int qHash(const Akonadi::Tag &tag);
+
+#include "imaptestbase.h"
+
+#include <AkonadiCore/Tag>
+
+#include <AkonadiCore/collectionquotaattribute.h>
+#include <AkonadiCore/attributefactory.h>
+#include <AkonadiCore/qtest_akonadi.h>
+#include <AkonadiCore/servermanager.h>
+#include <AkonadiCore/collectioncreatejob.h>
+//#include <AkonadiCore/virtualresource.h>
+#include <AkonadiCore/tagcreatejob.h>
+#include <kolabobject.h>
+
+#include "kolabchangeitemstagstask.h"
+#include "kolabhelpers.h"
+
+using namespace Akonadi;
+
+typedef QHash<QString, Akonadi::Item::List> Members;
+
+Q_DECLARE_METATYPE(TagListAndMembers);
+Q_DECLARE_METATYPE(Members);
+
+struct TestTagConverter : public TagConverter {
+    virtual KMime::Message::Ptr createMessage(const Akonadi::Tag &tag, const Akonadi::Item::List &items)
+    {
+        return KMime::Message::Ptr(new KMime::Message());
+    }
+};
+
+class TestChangeItemsTagsTask : public ImapTestBase
+{
+    Q_OBJECT
+
+private slots:
+
+    void initTestCase()
+    {
+        AkonadiTest::checkTestIsIsolated();
+    }
+
+    void testRetrieveTags_data()
+    {
+        Akonadi::VirtualResource *resource = new Akonadi::VirtualResource(QLatin1String("akonadi_knut_resource_0"), this);
+
+        Akonadi::Collection root;
+        root.setName(QLatin1String("akonadi_knut_resource_0"));
+        root.setContentMimeTypes(QStringList() << Akonadi::Collection::mimeType());
+        root.setParentCollection(Akonadi::Collection::root());
+        root.setRemoteId("root-id");
+        root = resource->createRootCollection(root);
+
+        Akonadi::Collection col;
+        col.setName("Configuration");
+        col.setContentMimeTypes(QStringList() << KolabHelpers::getMimeType(Kolab::ConfigurationType));
+        col.setRemoteId("/configuration");
+        col = resource->createCollection(col);
+
+        Akonadi::Collection mailcol;
+        mailcol.setName("INBOX");
+        mailcol.setContentMimeTypes(QStringList() << KMime::Message::mimeType());
+        mailcol.setRemoteId("/INBOX");
+        mailcol = resource->createCollection(mailcol);
+
+        Akonadi::Tag tag("tagname");
+        {
+            Akonadi::TagCreateJob *createJob = new Akonadi::TagCreateJob(tag);
+            AKVERIFYEXEC(createJob);
+            tag = createJob->tag();
+        }
+
+        Akonadi::Item item(KMime::Message::mimeType());
+        item.setRemoteId("20");
+        item.setTag(tag);
+        item = resource->createItem(item, mailcol);
+
+        QTest::addColumn< QList<QByteArray> >("scenario");
+        QTest::addColumn<QStringList>("callNames");
+        QTest::addColumn<Akonadi::Tag::List>("expectedTags");
+        QTest::addColumn<Members>("expectedMembers");
+        QTest::addColumn<DummyResourceState::Ptr>("resourceState");
+
+        {
+            QList<QByteArray> scenario;
+            scenario << defaultPoolConnectionScenario();
+
+            QStringList callNames;
+            callNames << "changeProcessed";
+
+            QHash<QString, Akonadi::Item::List> expectedMembers;
+
+            DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState);
+            state->setServerCapabilities(QStringList() << "METADATA" << "ACL");
+            state->setUserName("Hans");
+
+            QTest::newRow("nothing changed") << scenario << callNames << Akonadi::Tag::List() << expectedMembers << state;
+        }
+        {
+            KMime::Message::Ptr msg(new KMime::Message());
+
+            const QByteArray &content = msg->encodedContent(true);
+            QList<QByteArray> scenario;
+            scenario << defaultPoolConnectionScenario()
+                     << "C: A000003 APPEND \"configuration\"  {" + QByteArray::number(content.size()) + "}"
+                     << "S: A000003 OK append done [ APPENDUID 1239890035 65 ]";
+
+            QStringList callNames;
+            callNames << "changeProcessed";
+
+            Akonadi::Tag expectedTag = tag;
+            expectedTag.setRemoteId("7");
+
+            QHash<QString, Akonadi::Item::List> expectedMembers;
+            Akonadi::Item member;
+            member.setRemoteId("20");
+            member.setParentCollection(createCollectionChain("/INBOX"));
+            expectedMembers.insert(expectedTag.remoteId(), (Akonadi::Item::List() << member));
+
+            DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState);
+            state->setServerCapabilities(QStringList() << "METADATA" << "ACL");
+            state->setUserName("Hans");
+            state->setAddedTags(QSet<Akonadi::Tag>() << tag);
+
+            QTest::newRow("list single tag") << scenario << callNames << (Akonadi::Tag::List() << expectedTag) << expectedMembers << state;
+        }
+    }
+
+    void testRetrieveTags()
+    {
+        QFETCH(QList<QByteArray>, scenario);
+        QFETCH(QStringList, callNames);
+        QFETCH(Akonadi::Tag::List, expectedTags);
+        QFETCH(Members, expectedMembers);
+        QFETCH(DummyResourceState::Ptr, resourceState);
+
+        FakeServer server;
+        server.setScenario(scenario);
+        server.startAndWait();
+
+        SessionPool pool(1);
+
+        pool.setPasswordRequester(createDefaultRequester());
+        QVERIFY(pool.connect(createDefaultAccount()));
+        QVERIFY(waitForSignal(&pool, SIGNAL(connectDone(int,QString))));
+
+        KolabChangeItemsTagsTask *task = new KolabChangeItemsTagsTask(resourceState, QSharedPointer<TestTagConverter>(new TestTagConverter));
+
+        task->start(&pool);
+
+        QTRY_COMPARE(resourceState->calls().count(), callNames.size());
+        for (int i = 0; i < callNames.size(); i++) {
+            QString command = QString::fromUtf8(resourceState->calls().at(i).first);
+            QVariant parameter = resourceState->calls().at(i).second;
+
+            if (command == "cancelTask" && callNames[i] != "cancelTask") {
+                qCDebug(KOLABRESOURCE_LOG) << "Got a cancel:" << parameter.toString();
+            }
+
+            QCOMPARE(command, callNames[i]);
+
+            if (command == "tagsRetrieved") {
+                QPair<Akonadi::Tag::List, QHash<QString, Akonadi::Item::List> > pair = parameter.value<TagListAndMembers>();
+                Akonadi::Tag::List tags = pair.first;
+                QHash<QString, Akonadi::Item::List> members = pair.second;
+                QCOMPARE(tags.size(), expectedTags.size());
+                for (int i = 0; i < tags.size(); i++) {
+                    QCOMPARE(tags[i].name(), expectedTags[i].name());
+                    QCOMPARE(tags[i].remoteId(), expectedTags[i].remoteId());
+                    const Akonadi::Item::List memberlist = members.value(tags[i].remoteId());
+                    const Akonadi::Item::List expectedMemberlist = expectedMembers.value(tags[i].remoteId());
+                    QCOMPARE(memberlist.size(), expectedMemberlist.size());
+                    for (int i = 0; i < expectedMemberlist.size(); i++) {
+                        QCOMPARE(memberlist[i].remoteId(), expectedMemberlist[i].remoteId());
+                        Akonadi::Collection parent = memberlist[i].parentCollection();
+                        Akonadi::Collection expectedParent = expectedMemberlist[i].parentCollection();
+                        while (expectedParent.isValid()) {
+                            QCOMPARE(parent.remoteId(), expectedParent.remoteId());
+                            expectedParent = expectedParent.parentCollection();
+                            parent = parent.parentCollection();
+                        }
+                    }
+                }
+            }
+        }
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+
+    void testTagConverter()
+    {
+        TagConverter converter;
+        Akonadi::Item item(KMime::Message::mimeType());
+        KMime::Message::Ptr msg = KMime::Message::Ptr(new KMime::Message());
+        msg->subject(true)->from7BitString("subject");
+
+        msg->messageID(true)->from7BitString("<messageid@example.com>");
+        msg->date(true)->setDateTime(QDateTime(QDate(2014, 12, 10), QTime(9, 8, 7)));
+        item.setPayload<KMime::Message::Ptr>(msg);
+        item.setRemoteId(QLatin1String("20"));
+        item.setParentCollection(createCollectionChain("/INBOX"));
+        const QString member = KolabHelpers::createMemberUrl(item, QLatin1String("localuser@localhost"));
+        const QString expected = QLatin1String("imap:///user/localuser%40localhost/INBOX/20?message-id=%3Cmessageid%40example.com%3E&subject=subject&date=Wed%2C%2010%20Dec%202014%2009%3A08%3A07%20%2B0000");
+        QCOMPARE(member, expected);
+    }
+};
+
+QTEST_AKONADIMAIN(TestChangeItemsTagsTask)
+
+#include "testchangeitemstagstask.moc"
diff --git a/resources/kolab/tests/testretrievetagstask.cpp b/resources/kolab/tests/testretrievetagstask.cpp
new file mode 100644 (file)
index 0000000..e3940e4
--- /dev/null
@@ -0,0 +1,184 @@
+/*
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+#include "imaptestbase.h"
+
+#include "kolabretrievetagstask.h"
+
+#include <akonadi/collectionquotaattribute.h>
+#include <akonadi/attributefactory.h>
+#include <akonadi/qtest_akonadi.h>
+#include <akonadi/servermanager.h>
+#include <akonadi/collectioncreatejob.h>
+#include <akonadi/virtualresource.h>
+#include "kolabhelpers.h"
+#include <kolab/kolabobject.h>
+
+typedef QHash<QString, Akonadi::Item::List> Members;
+
+Q_DECLARE_METATYPE(TagListAndMembers);
+Q_DECLARE_METATYPE(Members);
+
+class TestRetrieveTagsTask : public ImapTestBase
+{
+    Q_OBJECT
+
+private slots:
+
+    void initTestCase()
+    {
+        AkonadiTest::checkTestIsIsolated();
+    }
+
+    void testRetrieveTags_data()
+    {
+        Akonadi::VirtualResource *resource = new Akonadi::VirtualResource(QLatin1String("akonadi_knut_resource_0"), this);
+
+        Akonadi::Collection root;
+        root.setName(QLatin1String("akonadi_knut_resource_0"));
+        root.setContentMimeTypes(QStringList() << Akonadi::Collection::mimeType());
+        root.setParentCollection(Akonadi::Collection::root());
+        root.setRemoteId("root-id");
+        root = resource->createRootCollection(root);
+
+        Akonadi::Collection col;
+        col.setName("Configuration");
+        col.setContentMimeTypes(QStringList() << KolabHelpers::getMimeType(Kolab::ConfigurationType));
+        col.setRemoteId("/configuration");
+        col = resource->createCollection(col);
+
+        Akonadi::Collection mailcol;
+        mailcol.setName("INBOX");
+        mailcol.setContentMimeTypes(QStringList() << KMime::Message::mimeType());
+        mailcol.setRemoteId("/INBOX");
+        mailcol = resource->createCollection(mailcol);
+
+        Akonadi::Item item(KMime::Message::mimeType());
+        item.setRemoteId("20");
+        item = resource->createItem(item, mailcol);
+
+        QTest::addColumn< QList<QByteArray> >("scenario");
+        QTest::addColumn<QStringList>("callNames");
+        QTest::addColumn<Akonadi::Tag::List>("expectedTags");
+        QTest::addColumn<Members>("expectedMembers");
+
+        QList<QByteArray> scenario;
+        QStringList callNames;
+
+        Akonadi::Tag tag;
+        tag.setName("tagname");
+        Kolab::KolabObjectWriter writer;
+        QStringList members;
+        members << QLatin1String("imap:///user/john.doe%40example.org/INBOX/20?message-id=%3Cf06aa3345a25005380b47547ad161d36%40lhm.klab.cc%3E&date=Tue%2C+12+Aug+2014+20%3A42%3A59+%2B0200&subject=Re%3A+test");
+        KMime::Message::Ptr msg = writer.writeTag(tag, members);
+        // qCDebug(KOLABRESOURCE_LOG) << msg->encodedContent();
+
+        const QByteArray &content = msg->encodedContent(true);
+        scenario.clear();
+        scenario << defaultPoolConnectionScenario()
+                 << "C: A000003 SELECT \"configuration\""
+                 << "S: A000003 OK select done"
+                 << "C: A000004 FETCH 1:* (RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID)"
+                 << "S: * 1 FETCH ( FLAGS (\\Seen) UID 7 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" "
+                 "RFC822.SIZE 75 BODY[] {" + QByteArray::number(content.size()) + "}\r\n"
+                 + content + " )"
+                 << "S: A000004 OK fetch done";
+
+        callNames.clear();
+        callNames << "tagsRetrieved";
+
+        Akonadi::Tag expectedTag = tag;
+        expectedTag.setRemoteId("7");
+
+        QHash<QString, Akonadi::Item::List> expectedMembers;
+        Akonadi::Item member;
+        member.setRemoteId("20");
+        member.setParentCollection(createCollectionChain("/INBOX"));
+        expectedMembers.insert(expectedTag.remoteId(), (Akonadi::Item::List() << member));
+
+        QTest::newRow("list single tag") << scenario << callNames << (Akonadi::Tag::List() << expectedTag) << expectedMembers;
+    }
+
+    void testRetrieveTags()
+    {
+        QFETCH(QList<QByteArray>, scenario);
+        QFETCH(QStringList, callNames);
+        QFETCH(Akonadi::Tag::List, expectedTags);
+        QFETCH(Members, expectedMembers);
+
+        FakeServer server;
+        server.setScenario(scenario);
+        server.startAndWait();
+
+        SessionPool pool(1);
+
+        pool.setPasswordRequester(createDefaultRequester());
+        QVERIFY(pool.connect(createDefaultAccount()));
+        QVERIFY(waitForSignal(&pool, SIGNAL(connectDone(int,QString))));
+
+        DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState);
+        state->setServerCapabilities(QStringList() << "METADATA" << "ACL");
+        state->setUserName("Hans");
+        KolabRetrieveTagTask *task = new KolabRetrieveTagTask(state, KolabRetrieveTagTask::RetrieveTags);
+
+        task->start(&pool);
+
+        QTRY_COMPARE(state->calls().count(), callNames.size());
+        for (int i = 0; i < callNames.size(); i++) {
+            QString command = QString::fromUtf8(state->calls().at(i).first);
+            QVariant parameter = state->calls().at(i).second;
+
+            if (command == "cancelTask" && callNames[i] != "cancelTask") {
+                qCDebug(KOLABRESOURCE_LOG) << "Got a cancel:" << parameter.toString();
+            }
+
+            QCOMPARE(command, callNames[i]);
+
+            if (command == "tagsRetrieved") {
+                QPair<Akonadi::Tag::List, QHash<QString, Akonadi::Item::List> > pair = parameter.value<TagListAndMembers>();
+                Akonadi::Tag::List tags = pair.first;
+                QHash<QString, Akonadi::Item::List> members = pair.second;
+                QCOMPARE(tags.size(), expectedTags.size());
+                for (int i = 0; i < tags.size(); i++) {
+                    QCOMPARE(tags[i].name(), expectedTags[i].name());
+                    QCOMPARE(tags[i].remoteId(), expectedTags[i].remoteId());
+                    const Akonadi::Item::List memberlist = members.value(tags[i].remoteId());
+                    const Akonadi::Item::List expectedMemberlist = expectedMembers.value(tags[i].remoteId());
+                    QCOMPARE(memberlist.size(), expectedMemberlist.size());
+                    for (int i = 0; i < expectedMemberlist.size(); i++) {
+                        QCOMPARE(memberlist[i].remoteId(), expectedMemberlist[i].remoteId());
+                        Akonadi::Collection parent = memberlist[i].parentCollection();
+                        Akonadi::Collection expectedParent = expectedMemberlist[i].parentCollection();
+                        while (expectedParent.isValid()) {
+                            QCOMPARE(parent.remoteId(), expectedParent.remoteId());
+                            expectedParent = expectedParent.parentCollection();
+                            parent = parent.parentCollection();
+                        }
+                    }
+                }
+            }
+        }
+
+        QVERIFY(server.isAllScenarioDone());
+
+        server.quit();
+    }
+};
+
+QTEST_AKONADIMAIN(TestRetrieveTagsTask, NoGUI)
+
+#include "testretrievetagstask.moc"
diff --git a/resources/kolab/tests/unittestenv/config-mysql-db.xml b/resources/kolab/tests/unittestenv/config-mysql-db.xml
new file mode 100644 (file)
index 0000000..011bc57
--- /dev/null
@@ -0,0 +1,8 @@
+<config>
+  <kdehome>kdehome</kdehome>
+  <confighome>xdgconfig-mysql.db</confighome>
+  <datahome>xdglocal</datahome>
+  <agent synchronize="true">akonadi_knut_resource</agent>
+  <envvar name="AKONADI_DISABLE_AGENT_AUTOSTART">true</envvar>
+  <envvar name="TESTRUNNER_DB_ENVIRONMENT">mysql</envvar>
+</config>
diff --git a/resources/kolab/tests/unittestenv/config-mysql-fs.xml b/resources/kolab/tests/unittestenv/config-mysql-fs.xml
new file mode 100644 (file)
index 0000000..4988fca
--- /dev/null
@@ -0,0 +1,8 @@
+<config>
+  <kdehome>kdehome</kdehome>
+  <confighome>xdgconfig-mysql.fs</confighome>
+  <datahome>xdglocal</datahome>
+  <agent synchronize="true">akonadi_knut_resource</agent>
+  <envvar name="AKONADI_DISABLE_AGENT_AUTOSTART">true</envvar>
+  <envvar name="TESTRUNNER_DB_ENVIRONMENT">mysql</envvar>
+</config>
diff --git a/resources/kolab/tests/unittestenv/config-postgresql-db.xml b/resources/kolab/tests/unittestenv/config-postgresql-db.xml
new file mode 100644 (file)
index 0000000..615e0d1
--- /dev/null
@@ -0,0 +1,8 @@
+<config>
+  <kdehome>kdehome</kdehome>
+  <confighome>xdgconfig-postgresql.db</confighome>
+  <datahome>xdglocal</datahome>
+  <agent synchronize="true">akonadi_knut_resource</agent>
+  <envvar name="AKONADI_DISABLE_AGENT_AUTOSTART">true</envvar>
+  <envvar name="TESTRUNNER_DB_ENVIRONMENT">postgresql</envvar>
+</config>
diff --git a/resources/kolab/tests/unittestenv/config-postgresql-fs.xml b/resources/kolab/tests/unittestenv/config-postgresql-fs.xml
new file mode 100644 (file)
index 0000000..3ba8d74
--- /dev/null
@@ -0,0 +1,8 @@
+<config>
+  <kdehome>kdehome</kdehome>
+  <confighome>xdgconfig-postgresql.fs</confighome>
+  <datahome>xdglocal</datahome>
+  <agent synchronize="true">akonadi_knut_resource</agent>
+  <envvar name="AKONADI_DISABLE_AGENT_AUTOSTART">true</envvar>
+  <envvar name="TESTRUNNER_DB_ENVIRONMENT">postgresql</envvar>
+</config>
diff --git a/resources/kolab/tests/unittestenv/config-sqlite-db.xml b/resources/kolab/tests/unittestenv/config-sqlite-db.xml
new file mode 100644 (file)
index 0000000..c36076f
--- /dev/null
@@ -0,0 +1,8 @@
+<config>
+  <kdehome>kdehome</kdehome>
+  <confighome>xdgconfig-sqlite.db</confighome>
+  <datahome>xdglocal</datahome>
+  <agent synchronize="true">akonadi_knut_resource</agent>
+  <envvar name="AKONADI_DISABLE_AGENT_AUTOSTART">true</envvar>
+  <envvar name="TESTRUNNER_DB_ENVIRONMENT">sqlite</envvar>
+</config>
diff --git a/resources/kolab/tests/unittestenv/kdehome/share/config/akonadi-firstrunrc b/resources/kolab/tests/unittestenv/kdehome/share/config/akonadi-firstrunrc
new file mode 100644 (file)
index 0000000..c5e90d8
--- /dev/null
@@ -0,0 +1,4 @@
+[ProcessedDefaults]
+defaultaddressbook=done
+defaultcalendar=done
+defaultnotebook=done
diff --git a/resources/kolab/tests/unittestenv/kdehome/share/config/akonadi_knut_resource_0rc b/resources/kolab/tests/unittestenv/kdehome/share/config/akonadi_knut_resource_0rc
new file mode 100644 (file)
index 0000000..61bb2f0
--- /dev/null
@@ -0,0 +1,4 @@
+[General]
+DataFile[$e]=$KDEHOME/testdata-res1.xml
+FileWatchingEnabled=false
+
diff --git a/resources/kolab/tests/unittestenv/kdehome/share/config/kdebugrc b/resources/kolab/tests/unittestenv/kdehome/share/config/kdebugrc
new file mode 100755 (executable)
index 0000000..bbd212c
--- /dev/null
@@ -0,0 +1,80 @@
+DisableAll=false
+
+[0]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[264]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[5250]
+InfoOutput=2
+
+[7009]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[7011]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[7012]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[7014]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[7021]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
diff --git a/resources/kolab/tests/unittestenv/kdehome/share/config/kdedrc b/resources/kolab/tests/unittestenv/kdehome/share/config/kdedrc
new file mode 100644 (file)
index 0000000..41d1781
--- /dev/null
@@ -0,0 +1,3 @@
+[General]
+CheckSycoca=false
+CheckFileStamps=false
diff --git a/resources/kolab/tests/unittestenv/kdehome/testdata-res1.xml b/resources/kolab/tests/unittestenv/kdehome/testdata-res1.xml
new file mode 100644 (file)
index 0000000..00ed642
--- /dev/null
@@ -0,0 +1,37 @@
+<knut>
+ <collection rid="1" name="res1" content="inode/directory">
+  <collection rid="2" name="INBOX" content="inode/directory,message/rfc822">
+   <item rid="A" mimetype="application/octet-stream">
+    <payload>testmailbody</payload>
+    <attribute type="HEAD">From: &lt;test@user.tst&gt;</attribute>
+    <flag>\SEEN</flag>
+    <flag>\FLAGGED</flag>
+    <flag>\DRAFT</flag>
+   </item>
+  </collection>
+  <collection rid="3" name="Calendar" content="inode/directory,message/rfc822">
+   <attribute type="collectionannotations">/shared/vendor/kolab/folder-type event</attribute>
+  </collection>
+  <collection rid="4" name="Contact" content="inode/directory,message/rfc822">
+   <attribute type="collectionannotations">/shared/vendor/kolab/folder-type contact</attribute>
+  </collection>
+  <collection rid="5" name="Notes" content="inode/directory,message/rfc822">
+   <attribute type="collectionannotations">/shared/vendor/kolab/folder-type note</attribute>
+  </collection>
+  <collection rid="6" name="Tasks" content="inode/directory,message/rfc822">
+   <attribute type="collectionannotations">/shared/vendor/kolab/folder-type task</attribute>
+  </collection>
+  <collection rid="7" name="Journal" content="inode/directory,message/rfc822">
+   <attribute type="collectionannotations">/shared/vendor/kolab/folder-type journal</attribute>
+  </collection>
+  <collection rid="8" name="Configuration" content="inode/directory,message/rfc822">
+   <attribute type="collectionannotations">/shared/vendor/kolab/folder-type configuration</attribute>
+  </collection>
+  <collection rid="9" name="Freebusy" content="inode/directory,message/rfc822">
+   <attribute type="collectionannotations">/shared/vendor/kolab/folder-type freebusy</attribute>
+  </collection>
+  <collection rid="10" name="Files" content="inode/directory,message/rfc822">
+   <attribute type="collectionannotations">/shared/vendor/kolab/folder-type file</attribute>
+  </collection>
+ </collection>
+</knut>
diff --git a/resources/kolab/tests/unittestenv/xdgconfig-mysql.db/akonadi/akonadiserverrc b/resources/kolab/tests/unittestenv/xdgconfig-mysql.db/akonadi/akonadiserverrc
new file mode 100644 (file)
index 0000000..fa9b2d4
--- /dev/null
@@ -0,0 +1,5 @@
+[%General]
+ExternalPayload=false
+
+[Search]
+Manager=Dummy
diff --git a/resources/kolab/tests/unittestenv/xdgconfig-mysql.fs/akonadi/akonadiserverrc b/resources/kolab/tests/unittestenv/xdgconfig-mysql.fs/akonadi/akonadiserverrc
new file mode 100644 (file)
index 0000000..a7bb0c2
--- /dev/null
@@ -0,0 +1,6 @@
+[%General]
+SizeThreshold=0
+ExternalPayload=true
+
+[Search]
+Manager=Dummy
diff --git a/resources/kolab/tests/unittestenv/xdgconfig-postgresql.db/akonadi/akonadiserverrc b/resources/kolab/tests/unittestenv/xdgconfig-postgresql.db/akonadi/akonadiserverrc
new file mode 100644 (file)
index 0000000..b2c8b1a
--- /dev/null
@@ -0,0 +1,9 @@
+[%General]
+Driver=QPSQL
+ExternalPayload=false
+
+[Search]
+Manager=Dummy
+
+[QPSQL]
+StartServer=true
diff --git a/resources/kolab/tests/unittestenv/xdgconfig-postgresql.fs/akonadi/akonadiserverrc b/resources/kolab/tests/unittestenv/xdgconfig-postgresql.fs/akonadi/akonadiserverrc
new file mode 100644 (file)
index 0000000..8333c73
--- /dev/null
@@ -0,0 +1,10 @@
+[%General]
+Driver=QPSQL
+SizeThreshold=0
+ExternalPayload=true
+
+[Search]
+Manager=Dummy
+
+[QPSQL]
+StartServer=true
diff --git a/resources/kolab/tests/unittestenv/xdgconfig-sqlite.db/akonadi/akonadiserverrc b/resources/kolab/tests/unittestenv/xdgconfig-sqlite.db/akonadi/akonadiserverrc
new file mode 100644 (file)
index 0000000..33e7f81
--- /dev/null
@@ -0,0 +1,10 @@
+[%General]
+# This is a slightly adjusted version of the QSQLITE driver from Qt
+# It is provided by akonadi itself
+Driver=QSQLITE3
+
+[Debug]
+Tracer=null
+
+[Search]
+Manager=Dummy
diff --git a/resources/kolab/updatemessagejob.cpp b/resources/kolab/updatemessagejob.cpp
new file mode 100644 (file)
index 0000000..e7bc2d6
--- /dev/null
@@ -0,0 +1,235 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "updatemessagejob.h"
+
+#include <KIMAP/AppendJob>
+#include <KIMAP/SearchJob>
+#include <KIMAP/SelectJob>
+#include <KIMAP/StoreJob>
+#include <KIMAP/FetchJob>
+#include <KIMAP/ImapSet>
+#include "kolabresource_debug.h"
+#include <KMime/Message>
+#include <replacemessagejob.h>
+
+#include "imapflags.h"
+
+//Check if the expected uid message is still there => no modification, replace message.
+//otherwise search for uptodate message by subject containing UID, merge contents, and replace message
+
+UpdateMessageJob::UpdateMessageJob(const KMime::Message::Ptr &msg, KIMAP::Session *session, const QByteArray &kolabUid, QSharedPointer<Merger> merger, const QString &mailbox, qint64 uidNext, qint64 oldUid, QObject *parent)
+    : KJob(parent),
+      mSession(session),
+      mMessage(msg),
+      mMailbox(mailbox),
+      mUidNext(uidNext),
+      mOldUid(oldUid),
+      mNewUid(-1),
+      mMessageId(msg->messageID()->asUnicodeString().toUtf8()),
+      mKolabUid(kolabUid),
+      mMerger(merger)
+{
+    mOldUids.add(oldUid);
+}
+
+void UpdateMessageJob::start()
+{
+    KIMAP::FetchJob *fetchJob = new KIMAP::FetchJob(mSession);
+
+    fetchJob->setSequenceSet(KIMAP::ImapSet(mOldUid));
+    fetchJob->setUidBased(true);
+
+    KIMAP::FetchJob::FetchScope scope;
+    scope.parts.clear();
+    scope.mode = KIMAP::FetchJob::FetchScope::Headers;
+    fetchJob->setScope(scope);
+
+    connect(fetchJob, SIGNAL(headersReceived(QString,
+                             QMap<qint64, qint64>,
+                             QMap<qint64, qint64>,
+                             QMap<qint64, KIMAP::MessageAttribute>,
+                             QMap<qint64, KIMAP::MessageFlags>,
+                             QMap<qint64, KIMAP::MessagePtr>)),
+            this, SLOT(onHeadersReceived(QString,
+                                         QMap<qint64, qint64>,
+                                         QMap<qint64, qint64>,
+                                         QMap<qint64, KIMAP::MessageAttribute>,
+                                         QMap<qint64, KIMAP::MessageFlags>,
+                                         QMap<qint64, KIMAP::MessagePtr>)));
+    connect(fetchJob, &KJob::result,
+            this, &UpdateMessageJob::onHeadersFetchDone);
+    fetchJob->start();
+
+}
+
+void UpdateMessageJob::onHeadersReceived(QString,
+        QMap<qint64, qint64> uids,
+        QMap<qint64, qint64>,
+        QMap<qint64, KIMAP::MessageAttribute>,
+        QMap<qint64, KIMAP::MessageFlags> flags,
+        QMap<qint64, KIMAP::MessagePtr>)
+{
+    //Filter deleted messages
+    foreach (qint64 number, uids.keys()) { //krazy:exclude=foreach
+        // const KMime::Message::Ptr msg = messages[number];
+        if (!flags[number].contains(ImapFlags::Deleted)) {
+            mFoundUids << uids[number];
+        }
+    }
+}
+
+void UpdateMessageJob::onHeadersFetchDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << "Failed to fetch message: " << job->errorString();
+    }
+    if (mFoundUids.size() >= 1) {
+        qCDebug(KOLABRESOURCE_LOG) << "Fast-forward update, replacing message.";
+        appendMessage();
+    } else {
+        if (mSession->selectedMailBox() != mMailbox) {
+            KIMAP::SelectJob *select = new KIMAP::SelectJob(mSession);
+            select->setMailBox(mMailbox);
+            connect(select, &KJob::result, this, &UpdateMessageJob::onSelectDone);
+            select->start();
+        } else {
+            searchForLatestVersion();
+        }
+    }
+}
+
+void UpdateMessageJob::onSelectDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << job->errorString();
+        setError(KJob::UserDefinedError);
+        emitResult();
+    } else {
+        searchForLatestVersion();
+    }
+}
+
+void UpdateMessageJob::searchForLatestVersion()
+{
+    KIMAP::SearchJob *search = new KIMAP::SearchJob(mSession);
+    search->setUidBased(true);
+    search->setSearchLogic(KIMAP::SearchJob::And);
+    search->addSearchCriteria(KIMAP::SearchJob::Header, "Subject " + mKolabUid);
+    connect(search, &KJob::result,
+            this, &UpdateMessageJob::onSearchDone);
+    search->start();
+}
+
+void UpdateMessageJob::onSearchDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << job->errorString();
+        setError(KJob::UserDefinedError);
+        emitResult();
+        return;
+    }
+
+    KIMAP::SearchJob *search = static_cast<KIMAP::SearchJob *>(job);
+
+    if (search->results().count() >= 1) {
+        mOldUids = KIMAP::ImapSet();
+        foreach (qint64 id, search->results()) {
+            mOldUids.add(id);
+        }
+
+        KIMAP::FetchJob *fetchJob = new KIMAP::FetchJob(mSession);
+        fetchJob->setSequenceSet(mOldUids);
+        fetchJob->setUidBased(true);
+
+        KIMAP::FetchJob::FetchScope scope;
+        scope.parts.clear();
+        scope.mode = KIMAP::FetchJob::FetchScope::Full;
+        fetchJob->setScope(scope);
+
+        connect(fetchJob, SIGNAL(headersReceived(QString,
+                                 QMap<qint64, qint64>,
+                                 QMap<qint64, qint64>,
+                                 QMap<qint64, KIMAP::MessageAttribute>,
+                                 QMap<qint64, KIMAP::MessageFlags>,
+                                 QMap<qint64, KIMAP::MessagePtr>)),
+                this, SLOT(onConflictingMessagesReceived(QString,
+                           QMap<qint64, qint64>,
+                           QMap<qint64, qint64>,
+                           QMap<qint64, KIMAP::MessageAttribute>,
+                           QMap<qint64, KIMAP::MessageFlags>,
+                           QMap<qint64, KIMAP::MessagePtr>)));
+        connect(fetchJob, &KJob::result,
+                this, &UpdateMessageJob::onConflictingMessageFetchDone);
+        fetchJob->start();
+    } else {
+        qCWarning(KOLABRESOURCE_LOG) << "failed to find latest version of object";
+        appendMessage();
+    }
+}
+
+void UpdateMessageJob::onConflictingMessagesReceived(QString,
+        QMap<qint64, qint64> uids,
+        QMap<qint64, qint64>,
+        QMap<qint64, KIMAP::MessageAttribute>,
+        QMap<qint64, KIMAP::MessageFlags> flags,
+        QMap<qint64, KIMAP::MessagePtr> messages)
+{
+    foreach (qint64 number, uids.keys()) { //krazy:exclude=foreach
+        if (!flags[number].contains(ImapFlags::Deleted)) {
+            mMessagesToMerge << messages[number];
+        }
+    }
+}
+
+void UpdateMessageJob::onConflictingMessageFetchDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << "Failed to retrieve messages to merge: " << job->errorString();
+    }
+    mMessage = mMerger->merge(mMessage, mMessagesToMerge);
+    appendMessage();
+}
+
+void UpdateMessageJob::appendMessage()
+{
+    const qint64 uidNext = -1;
+    ReplaceMessageJob *replace = new ReplaceMessageJob(mMessage, mSession, mMailbox, uidNext, mOldUids, this);
+    connect(replace, &KJob::result, this, &UpdateMessageJob::onReplaceDone);
+    replace->start();
+}
+
+void UpdateMessageJob::onReplaceDone(KJob *job)
+{
+    if (job->error()) {
+        qCWarning(KOLABRESOURCE_LOG) << job->errorString();
+        setError(KJob::UserDefinedError);
+        emitResult();
+        return;
+    }
+    ReplaceMessageJob *replaceJob = static_cast<ReplaceMessageJob *>(job);
+    mNewUid = replaceJob->newUid();
+    emitResult();
+}
+
+qint64 UpdateMessageJob::newUid() const
+{
+    return mNewUid;
+}
+
diff --git a/resources/kolab/updatemessagejob.h b/resources/kolab/updatemessagejob.h
new file mode 100644 (file)
index 0000000..0a7cfa3
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef UPDATEMESSAGEJOB_H
+#define UPDATEMESSAGEJOB_H
+
+#include <KJob>
+#include <KMime/Message>
+#include <KIMAP/Session>
+#include <KIMAP/FetchJob>
+
+struct Merger {
+    virtual ~Merger() {}
+    virtual KMime::Message::Ptr merge(KMime::Message::Ptr newMessage, QList<KMime::Message::Ptr> conflictingMessages) const = 0;
+};
+
+/**
+ * This job appends a message, marks the old one as deleted, and returns the uid of the appended message.
+ */
+class UpdateMessageJob : public KJob
+{
+    Q_OBJECT
+public:
+    UpdateMessageJob(const KMime::Message::Ptr &msg, KIMAP::Session *session, const QByteArray &kolabUid, QSharedPointer<Merger> merger, const QString &mailbox, qint64 uidNext = -1, qint64 oldUid = -1, QObject *parent = Q_NULLPTR);
+
+    qint64 newUid() const;
+
+    void start();
+
+private:
+    void searchForLatestVersion();
+    void appendMessage();
+
+private Q_SLOTS:
+    void onHeadersReceived(QString,
+                           QMap<qint64, qint64> uids,
+                           QMap<qint64, qint64>,
+                           QMap<qint64, KIMAP::MessageAttribute>,
+                           QMap<qint64, KIMAP::MessageFlags>,
+                           QMap<qint64, KIMAP::MessagePtr>);
+    void onHeadersFetchDone(KJob *job);
+    void onSelectDone(KJob *job);
+    void onSearchDone(KJob *job);
+    void onConflictingMessagesReceived(QString,
+                                       QMap<qint64, qint64> uids,
+                                       QMap<qint64, qint64>,
+                                       QMap<qint64, KIMAP::MessageAttribute>,
+                                       QMap<qint64, KIMAP::MessageFlags>,
+                                       QMap<qint64, KIMAP::MessagePtr>);
+    void onConflictingMessageFetchDone(KJob *job);
+    void onReplaceDone(KJob *job);
+
+private:
+    KIMAP::Session *mSession;
+    KMime::Message::Ptr mMessage;
+    const QString mMailbox;
+    qint64 mUidNext;
+    qint64 mOldUid;
+    KIMAP::ImapSet mOldUids;
+    qint64 mNewUid;
+    const QByteArray mMessageId;
+    const QByteArray mKolabUid;
+    QList<qint64> mFoundUids;
+    QList<KIMAP::MessagePtr> mMessagesToMerge;
+    QSharedPointer<Merger> mMerger;
+};
+
+#endif
+
diff --git a/resources/kolab/wizard/CMakeLists.txt b/resources/kolab/wizard/CMakeLists.txt
new file mode 100644 (file)
index 0000000..68dbb0f
--- /dev/null
@@ -0,0 +1,5 @@
+
+install(
+  FILES kolabwizard.desktop kolabwizard.es kolabwizard.ui kolabwizard2.ui
+  DESTINATION ${KDE_INSTALL_DATADIR}/akonadi/accountwizard/kolab
+)
diff --git a/resources/kolab/wizard/Messages.sh b/resources/kolab/wizard/Messages.sh
new file mode 100644 (file)
index 0000000..a41341f
--- /dev/null
@@ -0,0 +1,4 @@
+#! /usr/bin/env bash
+$EXTRACTRC *.ui >> rc.cpp
+$XGETTEXT *.cpp -o $podir/accountwizard_kolab.pot
+$XGETTEXT -kqsTr *.es -j -o $podir/accountwizard_kolab.pot
diff --git a/resources/kolab/wizard/kolabwizard.desktop b/resources/kolab/wizard/kolabwizard.desktop
new file mode 100644 (file)
index 0000000..26de8ad
--- /dev/null
@@ -0,0 +1,101 @@
+[Desktop Entry]
+Name=Kolab Groupware Server
+Name[bg]=Сървър Kolab Groupware
+Name[bs]=Server kolaborativnog softvera
+Name[ca]=Servidor de treball en grup Kolab
+Name[ca@valencia]=Servidor de treball en grup Kolab
+Name[cs]=Kolab Groupware server
+Name[da]=Kolab groupware-server
+Name[de]=Kolab Groupware-Server
+Name[el]=Εξυπηρετητής Groupware Kolab
+Name[en_GB]=Kolab Groupware Server
+Name[es]=Servidor de colaboración Kolab
+Name[et]=Kolabi grupitöö server
+Name[fi]=Kolab-työryhmäpalvelin
+Name[fr]=Serveur de logiciels de collaboration Kolab
+Name[ga]=Freastalaí Groupware Kolab
+Name[gl]=Servidor de Traballo en Grupo Kolab
+Name[hu]=Kolab csoportmunka-kiszolgáló
+Name[ia]=Servitor de Kolab Groupware
+Name[it]=Server di groupware Kolab
+Name[ja]=Kolab グループウェアサーバ
+Name[kk]=Kolab топтық іс сервері
+Name[km]=ម៉ាស៊ីន​បម្រើ Kolab Groupware
+Name[ko]=Kolab 그룹웨어 서버
+Name[lt]=Kolab grupinio darbo serveris
+Name[lv]=Kolab grupdarba serveris
+Name[nb]=Kolab groupware-tjener
+Name[nds]=Kolab-Arbeitkoppelserver
+Name[nl]=Kolab groupwareserver
+Name[nn]=Kolab Groupware-tenar
+Name[pl]=Serwer Groupware Kolab
+Name[pt]=Servidor de Groupware Kolab
+Name[pt_BR]=Servidor groupware Kolab
+Name[ro]=Server Kolab Groupware
+Name[ru]=Сервер совместной работы Kolab
+Name[sk]=Groupware Server Kolab
+Name[sl]=Strežnik za skupinsko delo Kolab
+Name[sr]=Колабов групверски сервер
+Name[sr@ijekavian]=Колабов групверски сервер
+Name[sr@ijekavianlatin]=Kolabov grupverski server
+Name[sr@latin]=Kolabov grupverski server
+Name[sv]=Kolab grupprogramserver
+Name[tr]=Kolab Groupware Sunucusu
+Name[uk]=Сервер групової роботи Kolab
+Name[x-test]=xxKolab Groupware Serverxx
+Name[zh_CN]=Kolab 群件服务器
+Name[zh_TW]=Kolab 群組伺服器
+Icon=kolab
+Comment=Kolab Groupware Server
+Comment[bg]=Сървър Kolab Groupware
+Comment[bs]=Server kolaborativnog softvera
+Comment[ca]=Servidor de treball en grup Kolab
+Comment[ca@valencia]=Servidor de treball en grup Kolab
+Comment[cs]=Kolab Groupware server
+Comment[da]=Kolab groupware-server
+Comment[de]=Kolab Groupware-Server
+Comment[el]=Εξυπηρετητής Groupware Kolab
+Comment[en_GB]=Kolab Groupware Server
+Comment[es]=Servidor de colaboración Kolab
+Comment[et]=Kolabi grupitöö server
+Comment[fi]=Kolab-työryhmäpalvelin
+Comment[fr]=Serveur de logiciels de collaboration Kolab
+Comment[ga]=Freastalaí Groupware Kolab
+Comment[gl]=Servidor de traballo en grupo Kolab
+Comment[hu]=Kolab csoportmunka-kiszolgáló
+Comment[ia]=Servitor de Kolab Groupware
+Comment[it]=Server di groupware Kolab
+Comment[ja]=Kolab グループウェアサーバ
+Comment[kk]=Kolab топтық іс сервері
+Comment[km]=ម៉ាស៊ីន​បម្រើ Kolab Groupware
+Comment[ko]=Kolab 그룹웨어 서버
+Comment[lt]=Kolab grupinio darbo serveris
+Comment[lv]=Kolab grupdarba serveris
+Comment[nb]=Kolab groupware-tjener 
+Comment[nds]=Kolab-Arbeitkoppelserver
+Comment[nl]=Kolab-groupware-server
+Comment[nn]=Kolab Groupware-tenar
+Comment[pl]=Serwer Kolab Groupware
+Comment[pt]=Servidor de 'Groupware' Kolab
+Comment[pt_BR]=Servidor groupware Kolab
+Comment[ro]=Server Kolab Groupware
+Comment[ru]=Сервер совместной работы Kolab
+Comment[sk]=Groupware Server Kolab
+Comment[sl]=Strežnik za skupinsko delo Kolab
+Comment[sr]=Колабов групверски сервер
+Comment[sr@ijekavian]=Колабов групверски сервер
+Comment[sr@ijekavianlatin]=Kolabov grupverski server
+Comment[sr@latin]=Kolabov grupverski server
+Comment[sv]=Kolab grupprogramserver
+Comment[tr]=Kolab Groupware Sunucusu
+Comment[uk]=Сервер групової роботи Kolab
+Comment[x-test]=xxKolab Groupware Serverxx
+Comment[zh_CN]=Kolab 群件服务器
+Comment[zh_TW]=Kolab 群組伺服器
+
+[Wizard]
+Type=message/rfc822,text/directory,text/calendar,text/x-vnd.akonadi.note
+Script=kolabwizard.es
+
+[Translate]
+Filename=accountwizard_kolab
diff --git a/resources/kolab/wizard/kolabwizard.es b/resources/kolab/wizard/kolabwizard.es
new file mode 100644 (file)
index 0000000..efd57c6
--- /dev/null
@@ -0,0 +1,322 @@
+/*
+    Copyright (c) 2014 Sandro Knauß <knauss@kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+// add this function to trim user input of whitespace when needed
+String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ""); };
+
+var page = Dialog.addPage("kolabwizard.ui", qsTr("Personal Settings"));
+var page2 = Dialog.addPage("kolabwizard2.ui", qsTr("Autoconfiguration"));
+var userChangedServerAddress = false;
+
+page.widget().nameEdit.text = SetupManager.name()
+page.widget().emailEdit.text = SetupManager.email()
+page.widget().passwordEdit.text = SetupManager.password()
+
+function guessServerName()
+{
+    var email = page.widget().emailEdit.text;
+    var pos = email.indexOf("@");
+    if (pos >= 0 && (pos + 1) < email.length) {
+      var server = email.slice(pos + 1, email.length);
+      return server;
+    }
+}
+
+function validateInput()
+{
+  if (page.widget().emailEdit.text.trim() == "" || page.widget().passwordEdit.text.trim() == "") {
+    page.setValid(false);
+  } else {
+    page.setValid(true);
+  }
+}
+
+//Server test
+servertest_running = false;
+//0 = not running, 1=submission, 2 = smtp, 3 = imap
+servertest_mode = 0;
+
+function testResultFail()
+{
+  testOk( -1 );
+}
+
+var imapRes;
+function testOk( arg )
+{
+    print("testOk arg =", arg);
+
+    if (servertest_mode < 3) {   // submission & smtp
+        if (arg == "tls" ) { // tls is really STARTTLS
+          smtp.setEncryption("TLS");
+          if (servertest_mode == 1) {   //submission port 587
+              smtp.setPort(587);
+          } else {
+              smtp.setPort(25);
+          }
+        } else if ( arg == "ssl" ) {    //only possible as smtps
+            smtp.setPort(465);
+            smtp.setEncryption("SSL");
+        } else if (servertest_mode == 2) { //test submission and smtp failed or only possible unencrypted -> set to standard value and open editor
+            smtp.setPort(587);
+            smtp.setEncryption("TLS");
+            smtp.setEditMode(true);
+        } else if (servertest_mode == 1) { // submission test failed -> start smtp request
+            servertest_mode = 2;
+            ServerTest.test(page2.widget().lineEditSmtp.text, "smtp");
+            return;
+        }
+
+        // start imap test
+        servertest_mode = 3;
+        if (page2.widget().lineEditImap.text) {
+            SetupManager.setupInfo(qsTr("Probing IMAP server..."));
+            ServerTest.test(page2.widget().lineEditImap.text, "imap");
+        } else {
+            SetupManager.execute();
+        }
+    } else if (servertest_mode == 3) {   //imap
+        if ( arg == "ssl" ) {
+          // The ENUM used for authentication (in the kolab resource only)
+          kolabRes.setOption( "Safety", "SSL" ); // SSL/TLS
+          kolabRes.setOption( "ImapPort", 993 );
+        } else if ( arg == "tls" ) { // tls is really STARTTLS
+          kolabRes.setOption( "Safety", "STARTTLS" );  // STARTTLS
+          kolabRes.setOption( "ImapPort", 143 );
+        } else {
+          // safe default fallback in case server test failed
+          kolabRes.setOption( "Safety", "STARTTLS" );
+          kolabRes.setOption( "ImapPort", 143 );
+          kolabRes.setEditMode(true);
+        }
+        SetupManager.execute();
+    } else {
+        print ("Unknown servertest_mode = ", servertest_mode);
+    }
+}
+
+var identity; // global so it can be accesed in setup and testOk
+
+var kolabRes;
+var smtp;
+var imapRes;
+
+var ac_mail;
+var ac_freebusy;
+var ac_ldap;
+
+var ac_mail_stat;
+var ac_freebusy_stat;
+var ac_ldap_stat;
+
+function checkAutoconfig()
+{
+    ac_mail_stat = false;
+    ac_freebusy_stat = false;
+    ac_ldap_stat = false;
+
+    page2.widget().lineEditImap.text = guessServerName();
+    page2.widget().lineEditSmtp.text = guessServerName();
+    page2.widget().lineEditImap.visible = false;
+    page2.widget().lineEditSmtp.visible = false;
+    page2.widget().lineEditImapLabel.visible = false;
+    page2.widget().lineEditSmtpLabel.visible = false;
+
+    page2.widget().checkBoxFreebusyEdit.text = qsTr("Create");
+    page2.widget().checkBoxLdapEdit.text = qsTr("Create");
+
+    ac_mail = SetupManager.ispDB('autoconfigkolabmail');
+    ac_mail.ispdbFinished.connect(mail_finished);
+    ac_mail.info.connect(mail_text);
+    ac_mail.setEmail(page.widget().emailEdit.text);
+    ac_mail.setPassword(page.widget().passwordEdit.text);
+    ac_mail.start();
+
+    ac_freebusy = SetupManager.ispDB('autoconfigkolabfreebusy');
+    ac_freebusy.ispdbFinished.connect(freebusy_finished);
+    ac_freebusy.info.connect(freebusy_text);
+    ac_freebusy.setEmail(page.widget().emailEdit.text);
+    ac_freebusy.setPassword(page.widget().passwordEdit.text);
+    ac_freebusy.start();
+
+    ac_ldap = SetupManager.ispDB('autoconfigkolabldap');
+    ac_ldap.ispdbFinished.connect(ldap_finished);
+    ac_ldap.info.connect(ldap_text);
+    ac_ldap.setEmail(page.widget().emailEdit.text);
+    ac_ldap.setPassword(page.widget().passwordEdit.text);
+    ac_ldap.start();
+}
+
+function mail_finished(stat) {
+    ac_mail_stat = stat;
+    if (stat) {
+        page2.widget().lineEditImap.visible = false;
+        page2.widget().lineEditSmtp.visible = false;
+        page2.widget().lineEditImapLabel.visible = false;
+        page2.widget().lineEditSmtpLabel.visible = false;
+    } else {
+        page2.widget().lineEditImap.visible = true;
+        page2.widget().lineEditSmtp.visible = true;
+        page2.widget().lineEditImapLabel.visible = true;
+        page2.widget().lineEditSmtpLabel.visible = true;
+    }
+}
+
+function mail_text(text) {
+    page2.widget().labelImapSearch.text = text;
+    page2.widget().labelSmtpSearch.text = text;
+}
+
+function freebusy_finished(stat) {
+    ac_freebusy_stat = stat;
+    if (stat) {
+        page2.widget().checkBoxFreebusyEdit.text = qsTr("Manual Edit");
+    }
+}
+
+function freebusy_text(text) {
+    page2.widget().labelFreebusySearch.text = text;
+}
+
+function ldap_finished(stat) {
+    ac_ldap_stat = stat;
+    if (stat) {
+        page2.widget().checkBoxLdapEdit.text = qsTr("Manual Edit");
+    }
+}
+
+function ldap_text(text) {
+    page2.widget().labelLdapSearch.text = text;
+}
+
+function setup()
+{
+    SetupManager.openWallet();
+    smtp = SetupManager.createTransport("smtp");
+    smtp.setEditMode(page2.widget().checkBoxSmtpEdit.checked);
+    smtp.setPassword(page.widget().passwordEdit.text);
+
+    if (ac_mail_stat) {
+        ac_mail.fillSmtpServer(0, smtp);
+    } else if (page2.widget().lineEditSmtp.text) {
+        var serverAddress = page2.widget().lineEditSmtp.text;
+        servertest_running = true;
+        servertest_mode = 1;
+        smtp.setName(serverAddress);
+        smtp.setHost(serverAddress);
+        smtp.setUsername(page.widget().emailEdit.text);
+        smtp.setAuthenticationType("plain");
+
+        SetupManager.setupInfo(qsTr("Probing SMTP server..."));
+        ServerTest.test( serverAddress, "submission" );   //probe port and encryption
+    }
+
+    for (i = 0; i < ac_mail.countIdentities(); i++) {
+        var j = SetupManager.createIdentity();
+        j.setTransport(smtp);
+        //templates
+        //drafts
+        //fcc
+        ac_mail.fillIdentitiy(i,j);
+        if (i == ac_mail.defaultIdentity()) {
+            identity = j;
+        }
+    }
+
+    if (ac_mail.countIdentities() == 0) {
+        identity = SetupManager.createIdentity();
+        identity.setEmail(page.widget().emailEdit.text);
+        identity.setRealName(page.widget().nameEdit.text);
+        identity.setTransport(smtp);
+    }
+
+    kolabRes = SetupManager.createResource("akonadi_kolab_resource");
+    kolabRes.setEditMode(page2.widget().checkBoxImapEdit.checked);
+    kolabRes.setOption("Password", page.widget().passwordEdit.text);
+    kolabRes.setOption("UseDefaultIdentity", false);
+    kolabRes.setOption("AccountIdentity", identity.uoid());
+    kolabRes.setOption("DisconnectedModeEnabled", true);
+    kolabRes.setOption("IntervalCheckTime", 60);
+    kolabRes.setOption("SubscriptionEnabled", true);
+    kolabRes.setOption("SieveSupport", true);
+
+    if (ac_mail_stat) {
+        ac_mail.fillImapServer(0, kolabRes);
+    } else if (page2.widget().lineEditImap.text) {
+        var serverAddress = page2.widget().lineEditImap.text;
+        kolabRes.setOption("ImapServer", serverAddress);
+        kolabRes.setOption("UserName", page.widget().emailEdit.text.trim());
+
+        if (!servertest_running) {
+            servertest_mode = 2;
+            servertest_running = true;
+            SetupManager.setupInfo(qsTr("Probing IMAP server..."));
+            ServerTest.test(serverAddress, "imap");
+        }       kolabRes.setOption("Authentication", 7);
+    }
+
+    if (ac_ldap_stat) {
+        for (i = 0; i < ac_ldap.countLdapServers(); i++) {
+            var ldap = SetupManager.createLdap();
+            ldap.setEditMode(page2.widget().checkBoxLdapEdit.checked);
+            ac_ldap.fillLdapServer(i,ldap);
+        }
+    } else if (page2.widget().checkBoxLdapEdit.checked) {
+        var ldap = SetupManager.createLdap();
+        ldap.setEditMode(page2.widget().checkBoxLdapEdit.checked);
+        ldap.setPassword(page.widget().passwordEdit.text);
+        ldap.setUser(page.widget().emailEdit.text);
+        ldap.setServer(guessServerName());
+    }
+
+    if (ac_freebusy_stat) {
+        var korganizer = SetupManager.createConfigFile("akonadi-calendarrc");
+        korganizer.setEditMode(page2.widget().checkBoxFreebusyEdit.checked);
+        korganizer.setEditName("freebusy");
+        korganizer.setName("korganizer");
+        ac_freebusy.fillFreebusyServer(0,korganizer);
+    } else if (page2.widget().checkBoxFreebusyEdit.checked) {
+        var korganizer = SetupManager.createConfigFile("akonadi-calendarrc");
+        korganizer.setEditMode(page2.widget().checkBoxFreebusyEdit.checked);
+        korganizer.setEditName("freebusy");
+        korganizer.setName( "korganizer" );
+        korganizer.setConfig( "FreeBusy Retrieve", "FreeBusyFullDomainRetrieval","true");
+        korganizer.setConfig( "FreeBusy Retrieve", "FreeBusyRetrieveAuto", "true" );
+        korganizer.setConfig( "FreeBusy Retrieve", "FreeBusyRetrieveUrl", "https://" + guessServerName()  + "/freebusy/" );
+    }
+
+    if (!servertest_running) {
+        SetupManager.execute();
+    }
+}
+
+try {
+  ServerTest.testFail.connect(testResultFail);
+  ServerTest.testResult.connect(testOk);
+
+  page.widget().emailEdit.textChanged.connect(validateInput);
+  page.widget().passwordEdit.textChanged.connect(validateInput);
+
+  page.pageLeftNext.connect(checkAutoconfig);
+  page2.pageLeftNext.connect(setup);
+} catch (e) {
+  print(e);
+}
+
+validateInput();
diff --git a/resources/kolab/wizard/kolabwizard.ui b/resources/kolab/wizard/kolabwizard.ui
new file mode 100644 (file)
index 0000000..b7c0890
--- /dev/null
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>kolabWizard_step1</class>
+ <widget class="QWidget" name="kolabWizard_step1">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>368</width>
+    <height>125</height>
+   </rect>
+  </property>
+  <layout class="QFormLayout" name="formLayout">
+   <property name="fieldGrowthPolicy">
+    <enum>QFormLayout::ExpandingFieldsGrow</enum>
+   </property>
+   <item row="0" column="0">
+    <widget class="QLabel" name="nameLabel">
+     <property name="text">
+      <string>&amp;Name:</string>
+     </property>
+     <property name="buddy">
+      <cstring>nameEdit</cstring>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="1">
+    <widget class="KLineEdit" name="nameEdit"/>
+   </item>
+   <item row="2" column="0">
+    <widget class="QLabel" name="emailLabel">
+     <property name="text">
+      <string>&amp;Email:</string>
+     </property>
+     <property name="buddy">
+      <cstring>emailEdit</cstring>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="1">
+    <widget class="KLineEdit" name="emailEdit"/>
+   </item>
+   <item row="4" column="0">
+    <widget class="QLabel" name="passwordLabel">
+     <property name="text">
+      <string>&amp;Password:</string>
+     </property>
+     <property name="buddy">
+      <cstring>passwordEdit</cstring>
+     </property>
+    </widget>
+   </item>
+   <item row="4" column="1">
+    <widget class="KLineEdit" name="passwordEdit">
+     <property name="echoMode">
+      <enum>QLineEdit::Password</enum>
+     </property>
+    </widget>
+   </item>
+   <item row="6" column="0">
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>Kolab Version:</string>
+     </property>
+     <property name="buddy">
+      <cstring>versionComboBox</cstring>
+     </property>
+    </widget>
+   </item>
+   <item row="6" column="1">
+    <widget class="QComboBox" name="versionComboBox">
+     <property name="currentIndex">
+      <number>1</number>
+     </property>
+     <item>
+      <property name="text">
+       <string>v2</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>v3</string>
+      </property>
+     </item>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KLineEdit</class>
+   <extends>QLineEdit</extends>
+   <header>klineedit.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/resources/kolab/wizard/kolabwizard2.ui b/resources/kolab/wizard/kolabwizard2.ui
new file mode 100644 (file)
index 0000000..fa95dd9
--- /dev/null
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>kolabWizard_step2</class>
+ <widget class="QWidget" name="kolabWizard_step2">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>625</width>
+    <height>134</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string/>
+  </property>
+  <layout class="QFormLayout" name="formLayout">
+   <item row="0" column="0">
+    <widget class="QLabel" name="imapLabel">
+     <property name="text">
+      <string>IMAP:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="1">
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QLabel" name="labelImapSearch">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="text">
+        <string/>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QLabel" name="lineEditImapLabel">
+       <property name="text">
+        <string>IMAP Server Name:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QLineEdit" name="lineEditImap"/>
+     </item>
+     <item>
+      <widget class="QCheckBox" name="checkBoxImapEdit">
+       <property name="enabled">
+        <bool>true</bool>
+       </property>
+       <property name="text">
+        <string>Manual Edit</string>
+       </property>
+       <property name="checked">
+        <bool>false</bool>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item row="5" column="1">
+    <layout class="QHBoxLayout" name="horizontalLayout_4">
+     <item>
+      <widget class="QLabel" name="labelFreebusySearch">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="text">
+        <string/>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QCheckBox" name="checkBoxFreebusyEdit">
+       <property name="enabled">
+        <bool>true</bool>
+       </property>
+       <property name="text">
+        <string>Create</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item row="5" column="0">
+    <widget class="QLabel" name="labelFreebusy">
+     <property name="text">
+      <string>Freebusy:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="0">
+    <widget class="QLabel" name="labelSmtp">
+     <property name="text">
+      <string>SMTP:</string>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="1">
+    <layout class="QHBoxLayout" name="horizontalLayout_3">
+     <item>
+      <widget class="QLabel" name="labelSmtpSearch">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="text">
+        <string/>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QLabel" name="lineEditSmtpLabel">
+       <property name="text">
+        <string>SMTP Server Name:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QLineEdit" name="lineEditSmtp"/>
+     </item>
+     <item>
+      <widget class="QCheckBox" name="checkBoxSmtpEdit">
+       <property name="enabled">
+        <bool>true</bool>
+       </property>
+       <property name="text">
+        <string>Manual Edit</string>
+       </property>
+       <property name="checked">
+        <bool>false</bool>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item row="3" column="1">
+    <layout class="QHBoxLayout" name="horizontalLayout_2">
+     <item>
+      <widget class="QLabel" name="labelLdapSearch">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="text">
+        <string/>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QCheckBox" name="checkBoxLdapEdit">
+       <property name="text">
+        <string>Create</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item row="3" column="0">
+    <widget class="QLabel" name="labelLdap">
+     <property name="text">
+      <string>LDAP:</string>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/resources/maildir/CMakeLists.txt b/resources/maildir/CMakeLists.txt
new file mode 100644 (file)
index 0000000..c744816
--- /dev/null
@@ -0,0 +1,59 @@
+project(maildir)
+include_directories(
+    ${CMAKE_CURRENT_SOURCE_DIR}/libmaildir
+    ${kdepim-runtime_SOURCE_DIR}
+)
+
+
+if (BUILD_TESTING)
+   add_subdirectory( autotests )
+endif()
+add_subdirectory( wizard )
+
+# maildir access library
+add_subdirectory(libmaildir)
+
+
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_maildir_resource\")
+
+########### next target ###############
+set( maildirresource_SRCS
+  main.cpp
+  maildirresource.cpp
+  configdialog.cpp
+  retrieveitemsjob.cpp
+)
+ecm_qt_declare_logging_category(maildirresource_SRCS HEADER maildirresource_debug.h IDENTIFIER MAILDIRRESOURCE_LOG CATEGORY_NAME log_maildirresource)
+
+ki18n_wrap_ui(maildirresource_SRCS settings.ui)
+
+kconfig_add_kcfg_files(maildirresource_SRCS settings.kcfgc)
+
+kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/maildirresource.kcfg org.kde.Akonadi.Maildir.Settings)
+
+qt5_add_dbus_adaptor(maildirresource_SRCS
+  ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.Maildir.Settings.xml settings.h Akonadi_Maildir_Resource::MaildirSettings maildirsettingsadaptor MaildirSettingsAdaptor
+)
+
+add_executable(akonadi_maildir_resource ${maildirresource_SRCS})
+
+target_link_libraries(akonadi_maildir_resource
+    maildir
+    folderarchivesettings
+    KF5::AkonadiCore
+    KF5::AkonadiMime
+    KF5::KIOWidgets
+    KF5::Mime
+    KF5::AkonadiAgentBase
+    KF5::DBusAddons
+    KF5::WindowSystem
+    KF5::I18n
+    KF5::ConfigWidgets
+)
+
+install(TARGETS akonadi_maildir_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.Maildir.Settings.xml
+        DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR})
+
+install( FILES maildirresource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents" )
+
diff --git a/resources/maildir/Messages.sh b/resources/maildir/Messages.sh
new file mode 100644 (file)
index 0000000..ed3a4ea
--- /dev/null
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+$EXTRACTRC *.ui *.kcfg >> rc.cpp
+$XGETTEXT libmaildir/*.cpp *.cpp -o $podir/akonadi_maildir_resource.pot
diff --git a/resources/maildir/autotests/CMakeLists.txt b/resources/maildir/autotests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..19c2bda
--- /dev/null
@@ -0,0 +1,46 @@
+include(ECMMarkAsTest)
+
+find_package(Qt5Test CONFIG REQUIRED)
+
+set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} )
+
+# Stolen from kdepimlibs/akonadi/tests
+macro(add_akonadi_isolated_test _source)
+  get_filename_component(_targetName ${_source} NAME_WE)
+  set(_srcList ${_source} )
+
+  add_executable(${_targetName} ${_srcList})
+  ecm_mark_as_test(${_targetName})
+  target_link_libraries(${_targetName}
+    Qt5::Test
+    KF5::AkonadiCore
+    KF5::AkonadiMime
+    KF5::MailTransport
+    KF5::Mime
+    Qt5::DBus
+    Qt5::Widgets
+  )
+
+  # based on kde4_add_unit_test
+  if (WIN32)
+    get_target_property( _loc ${_targetName} LOCATION )
+    set(_executable ${_loc}.bat)
+  else ()
+    set(_executable ${EXECUTABLE_OUTPUT_PATH}/${_targetName})
+  endif ()
+  if (UNIX)
+    set(_executable ${_executable}.shell)
+  endif ()
+
+  find_program(_testrunner akonaditest)
+
+  if (KDEPIM_RUN_ISOLATED_TESTS)
+    add_test( maildir-${_targetName} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config.xml ${_executable} )
+  endif ()
+endmacro(add_akonadi_isolated_test)
+
+
+
+add_akonadi_isolated_test( synctest.cpp )
+akonadi_add_resourcetest( maildir maildir.js )
+
diff --git a/resources/maildir/autotests/maildir-empty.xml b/resources/maildir/autotests/maildir-empty.xml
new file mode 100644 (file)
index 0000000..589bd6f
--- /dev/null
@@ -0,0 +1,5 @@
+<knut>
+  <collection content="message/rfc822,inode/directory" rid="newmaildir" name="akonadi_maildir_resource_1" >
+    <attribute type="AccessRights" >wcdC</attribute>
+  </collection>
+</knut>
diff --git a/resources/maildir/autotests/maildir-step1.xml b/resources/maildir/autotests/maildir-step1.xml
new file mode 100644 (file)
index 0000000..faa20e2
--- /dev/null
@@ -0,0 +1,29 @@
+<knut>
+  <collection content="message/rfc822,inode/directory" rid="newmaildir" name="akonadi_maildir_resource_1" >
+    <attribute type="AccessRights" >wcdC</attribute>
+    <collection content="message/rfc822,inode/directory" rid="test folder" name="test folder" >
+      <item mimetype="message/rfc822" rid="{e0473600-7911-4606-bc45-e50eeaf22458}" >
+        <payload>From: Volker Krause &lt;vkrause@kde.org>
+To: kde-commits@kde.org
+Subject: playground/pim/akonaditest/resourcetester
+MIME-Version: 1.0
+Content-Type: text/plain;
+  charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Date: Sun, 22 Mar 2009 12:50:30 +0000
+Message-Id: &lt;1237726230.394911.25706.nullmailer@svn.kde.org>
+
+SVN commit 942677 by vkrause:
+
+Add a safety timeout in case we do not receive the synchronized() signal
+or the resource hangs during syncing. The first seems to happen randomly
+if syncing is extremely fast.
+
+
+ M  +40 -0     resourcesynchronizationjob.cpp  
+ M  +1 -1      resourcesynchronizationjob.h  
+</payload>
+      </item>
+    </collection>
+  </collection>
+</knut>
diff --git a/resources/maildir/autotests/maildir-step2.xml b/resources/maildir/autotests/maildir-step2.xml
new file mode 100644 (file)
index 0000000..538885a
--- /dev/null
@@ -0,0 +1,29 @@
+<knut>
+  <collection content="message/rfc822,inode/directory" rid="newmaildir" name="akonadi_maildir_resource_223" >
+    <attribute type="AccessRights" >wcdC</attribute>
+    <collection content="message/rfc822,inode/directory" rid="changed folder" name="changed folder" >
+      <item mimetype="message/rfc822" rid="{998d3576-4a2f-45ca-8a3e-43c5d41f7d70}" >
+        <payload>From: Volker Krause &lt;vkrause@kde.org>
+To: kde-commits@kde.org
+Subject: playground/pim/akonaditest/resourcetester
+MIME-Version: 1.0
+Content-Type: text/plain;
+  charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Date: Sun, 22 Mar 2009 12:50:30 +0000
+Message-Id: &lt;1237726230.394911.25706.nullmailer@svn.kde.org>
+
+SVN commit 942677 by vkrause:
+
+Add a safety timeout in case we do not receive the synchronized() signal
+or the resource hangs during syncing. The first seems to happen randomly
+if syncing is extremely fast.
+
+
+ M  +40 -0     resourcesynchronizationjob.cpp  
+ M  +1 -1      resourcesynchronizationjob.h  
+</payload>
+      </item>
+    </collection>
+  </collection>
+</knut>
diff --git a/resources/maildir/autotests/maildir.js b/resources/maildir/autotests/maildir.js
new file mode 100644 (file)
index 0000000..a05eee4
--- /dev/null
@@ -0,0 +1,63 @@
+Resource.setType( "akonadi_maildir_resource" );
+
+// read test
+Resource.setPathOption( "Path", "maildir/root" );
+Resource.create();
+
+XmlOperations.setXmlFile( "maildir.xml" );
+XmlOperations.setRootCollections( Resource.identifier() );
+XmlOperations.setNormalizeRemoteIds( true );
+XmlOperations.ignoreCollectionField( "Name" );
+XmlOperations.assertEqual();
+
+Resource.destroy();
+
+// empty maildir
+Resource.setPathOption( "Path", "newmaildir" );
+Resource.create();
+
+XmlOperations.setXmlFile( "maildir-empty.xml" );
+XmlOperations.setRootCollections( Resource.identifier() );
+XmlOperations.assertEqual();
+
+// folder creation
+CollectionTest.setParent( Resource.identifier() );
+CollectionTest.addContentType( "message/rfc822" );
+CollectionTest.setName( "test folder" );
+CollectionTest.create();
+
+// item creation
+ItemTest.setParentCollection( Resource.identifier() + "/test folder" );
+ItemTest.setMimeType( "message/rfc822" );
+ItemTest.setPayloadFromFile( "testmail.mbox" );
+ItemTest.create();
+
+Resource.recreate();
+
+XmlOperations.setXmlFile( "maildir-step1.xml" );
+XmlOperations.setRootCollections( Resource.identifier() );
+XmlOperations.setItemKey( "None" );
+XmlOperations.ignoreItemField( "RemoteId" );
+XmlOperations.assertEqual();
+
+// folder modification
+CollectionTest.setCollection( Resource.identifier() + "/test folder" );
+CollectionTest.setName( "changed folder" );
+CollectionTest.update();
+
+Resource.recreate();
+
+XmlOperations.setXmlFile( "maildir-step2.xml" );
+XmlOperations.setRootCollections( Resource.identifier() );
+XmlOperations.assertEqual();
+
+// folder deletion 
+CollectionTest.setCollection( Resource.identifier() + "/changed folder" );
+CollectionTest.remove();
+
+Resource.recreate();
+
+XmlOperations.setXmlFile( "maildir-empty.xml" );
+XmlOperations.setRootCollections( Resource.identifier() );
+XmlOperations.assertEqual();
+
diff --git a/resources/maildir/autotests/maildir.xml b/resources/maildir/autotests/maildir.xml
new file mode 100644 (file)
index 0000000..8caf7ea
--- /dev/null
@@ -0,0 +1,732 @@
+<knut>
+  <collection content="message/rfc822,inode/directory" rid="maildir/root" name="akonadi_maildir_resource_1" >
+    <attribute type="AccessRights" >wcdC</attribute>
+    <collection content="message/rfc822,inode/directory" rid="child2" name="child2" />
+    <collection content="message/rfc822,inode/directory" rid="child1" name="child1" >
+      <collection content="message/rfc822,inode/directory" rid="grandchild" name="grandchild" >
+        <item mimetype="message/rfc822" rid="1237726881.6570.rfoxg!2,S" >
+          <payload>Return-Path: &lt;commitfilter@new.kstuff.org>
+Received: from localhost (localhost [127.0.0.1])
+        by smykowski.kdab.net (Cyrus v2.2.12) with LMTPA;
+        Sun, 22 Mar 2009 12:10:48 +0100
+X-Sieve: CMU Sieve 2.2
+Received: from localhost (localhost [127.0.0.1])
+       by smykowski.kdab.net (Postfix) with ESMTP id 4BDF8E6C790
+       for &lt;asok@kdab.net>; Sun, 22 Mar 2009 12:10:48 +0100 (CET)
+Received: from smykowski.kdab.net ([127.0.0.1])
+ by localhost (smykowski.kdab.net [127.0.0.1]) (amavisd-new, port 10024)
+ with ESMTP id 03694-02 for &lt;asok@kdab.net>;
+ Sun, 22 Mar 2009 12:10:45 +0100 (CET)
+Received: from localhost (localhost [127.0.0.1])
+       by smykowski.kdab.net (Postfix) with ESMTP id B289FE6C79B
+       for &lt;asok@kdab.net>; Sun, 22 Mar 2009 12:10:45 +0100 (CET)
+Received: from kdeget.osuosl.org (kdeget.osuosl.org [140.211.166.77])
+       by smykowski.kdab.net (Postfix) with ESMTP id 3B465E6C790
+       for &lt;asok@kdab.net>; Sun, 22 Mar 2009 12:10:45 +0100 (CET)
+Received: from ktown.kde.org ([131.246.120.250])
+       by kdeget.osuosl.org with smtp (Exim 4.63)
+       (envelope-from &lt;kde-commits-bounces-+commitfilter=new.kstuff.org@kde.org>)
+       id 1LlLfE-0001OT-K7
+       for commitfilter@new.kstuff.org; Sun, 22 Mar 2009 12:16:25 +0100
+Received: (qmail 23006 invoked by uid 72); 22 Mar 2009 11:16:19 -0000
+Received: (qmail 22986 invoked from network); 22 Mar 2009 11:16:14 -0000
+Received: from unknown (HELO office.kde.org) (195.135.221.67)
+       by ktown.kde.org with SMTP; 22 Mar 2009 11:16:11 -0000
+Received: from svn.kde.org (localhost [127.0.0.1])
+       by office.kde.org (Postfix) with SMTP id 85EE718E
+       for &lt;kde-commits@kde.org>; Sun, 22 Mar 2009 12:16:12 +0100 (CET)
+Received: (nullmailer pid 13467 invoked by uid 30);
+       Sun, 22 Mar 2009 11:16:12 -0000
+From: Volker Krause &lt;vkrause@kde.org>
+To: kde-commits@kde.org
+Subject: playground/pim/akonaditest/resourcetester
+X-Commit-Directories: (0) trunk/playground/pim/akonaditest/resourcetester
+       trunk/playground/pim/akonaditest/resourcetester/tests
+MIME-Version: 1.0
+Content-Type: text/plain;
+  charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Date: Sun, 22 Mar 2009 11:16:12 +0000
+Message-Id: &lt;1237720572.493438.13466.nullmailer@svn.kde.org>
+X-BeenThere: kde-commits@kde.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+Reply-To: kde-commits@kde.org
+List-Id: Notification of KDE commits &lt;kde-commits.kde.org>
+List-Unsubscribe: &lt;https://mail.kde.org/mailman/listinfo/kde-commits>,
+       &lt;mailto:kde-commits-request@kde.org?subject=unsubscribe>
+List-Post: &lt;mailto:kde-commits@kde.org>
+List-Help: &lt;mailto:kde-commits-request@kde.org?subject=help>
+List-Subscribe: &lt;https://mail.kde.org/mailman/listinfo/kde-commits>,
+       &lt;mailto:kde-commits-request@kde.org?subject=subscribe>
+X-Virus-Scanned: by amavisd-new at kdab.net
+X-Kolab-Scheduling-Message: FALSE
+X-UID: 26666
+X-Length: 11240
+Status: RO
+X-Status: ORC
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+SVN commit 942640 by vkrause:
+
+Allow to specifiy the collection property used to identify corresponding
+collections.
+
+
+ M  +2 -0      tests/vcardtest.js  
+ M  +1 -1      tests/vcardtest.xml  
+ M  +29 -14    xmloperations.cpp  
+ M  +46 -0     xmloperations.h  
+
+
+--- trunk/playground/pim/akonaditest/resourcetester/tests/vcardtest.js #942639:942640
+@@ -4,6 +4,8 @@
+ XmlOperations.setXmlFile( "vcardtest.xml" );
+ XmlOperations.setRootCollections( Resource.identifier() );
++XmlOperations.setCollectionKey( "None" ); // we only expect one collection
+ XmlOperations.ignoreCollectionField( "Name" ); // name is the resource identifier and thus unpredictable
++XmlOperations.ignoreCollectionField( "RemoteId" ); // remote id is the absolute path
+ XmlOperations.assertEqual();
+--- trunk/playground/pim/akonaditest/resourcetester/tests/vcardtest.xml #942639:942640
+@@ -1,5 +1,5 @@
+ &lt;knut>
+-  &lt;collection rid="/k/kde4/src/playground/pim/akonaditest/resourcetester/tests/vcardtest.vcf" name="akonadi_vcard_resource_0" content="text/directory">
++  &lt;collection rid="vcardtest.vcf" name="akonadi_vcard_resource_0" content="text/directory">
+     &lt;attribute type="AccessRights" >wcdW&lt;/attribute>
+     &lt;attribute type="ENTITYDISPLAY" >("vcardtest.vcf" "office-address-book")&lt;/attribute>
+     &lt;item rid="bb2slGmqxb" mimetype="text/directory">
+--- trunk/playground/pim/akonaditest/resourcetester/xmloperations.cpp #942639:942640
+@@ -31,9 +31,6 @@
+ #include &lt;QFileInfo>
+ #include &lt;QStringList>
+-#include &lt;boost/bind.hpp>
+-#include &lt;algorithm>
+-
+ using namespace Akonadi;
+ template &lt;typename T> QTextStream&amp; operator&lt;&lt;( QTextStream &amp;s, const QSet&lt;T> &amp;set )
+@@ -53,7 +50,8 @@
+ XmlOperations::XmlOperations(QObject* parent) :
+   QObject( parent ),
+-  mCollectionFields( 0xFF )
++  mCollectionFields( 0xFF ),
++  mCollectionKey( RemoteId )
+ {
+ }
+@@ -99,6 +97,17 @@
+   return mErrorMsg;
+ }
++void XmlOperations::setCollectionKey(XmlOperations::CollectionField field)
++{
++  mCollectionKey = field;
++}
++
++void XmlOperations::setCollectionKey(const QString&amp; fieldName)
++{
++  const QMetaEnum me = metaObject()->enumerator( metaObject()->indexOfEnumerator( "CollectionField" ) );
++  setCollectionKey( static_cast&lt;CollectionField>( me.keyToValue( fieldName.toLatin1() ) ) );
++}
++
+ void XmlOperations::ignoreCollectionField(XmlOperations::CollectionField field)
+ {
+   mCollectionFields = mCollectionFields &amp; ~field;
+@@ -137,8 +146,20 @@
+ {
+   Collection::List cols( _cols );
+   Collection::List refCols( _refCols );
+-  std::sort( cols.begin(), cols.end(), boost::bind( &amp;Collection::remoteId, _1 ) &lt; boost::bind( &amp;Collection::remoteId, _2 ) );
+-  std::sort( refCols.begin(), refCols.end(), boost::bind( &amp;Collection::remoteId, _1 ) &lt; boost::bind( &amp;Collection::remoteId, _2 ) );
++  switch ( mCollectionKey ) {
++    case RemoteId:
++      sortCollectionList( cols, &amp;Collection::remoteId );
++      sortCollectionList( refCols, &amp;Collection::remoteId );
++      break;
++    case Name:
++      sortCollectionList( cols, &amp;Collection::name );
++      sortCollectionList( refCols, &amp;Collection::name );
++      break;
++    case None:
++      break;
++    default:
++      Q_ASSERT( false );
++  }
+   for ( int i = 0; i &lt; cols.count(); ++i ) {
+     const Collection col = cols.at( i );
+@@ -148,11 +169,6 @@
+     }
+     const Collection refCol = refCols.at( i );
+-    if ( col.remoteId() != refCol.remoteId() ) {
+-      mErrorMsg = QString::fromLatin1( "Collection with remote id '%1' is missing." ).arg( refCol.remoteId() );
+-      return false;
+-    }
+-
+     if ( !compareCollection( col, refCol ) )
+       return false;
+   }
+@@ -177,14 +193,13 @@
+ bool XmlOperations::compareCollection(const Collection&amp; _col, const Collection&amp; _refCol)
+ {
+-  Q_ASSERT( _col.remoteId() == _refCol.remoteId() );
+-
+   // normalize
+   Collection col( normalize( _col ) );
+   Collection refCol( normalize( _refCol ) );
+   
+   // compare the two collections
+-  if ( !compareValue( col, refCol, &amp;Collection::contentMimeTypes, ContentMimeType ) ||
++  if ( !compareValue( col, refCol, &amp;Collection::remoteId, RemoteId ) ||
++       !compareValue( col, refCol, &amp;Collection::contentMimeTypes, ContentMimeType ) ||
+        !compareValue( col, refCol, &amp;Collection::name, Name ) )
+     return false;
+--- trunk/playground/pim/akonaditest/resourcetester/xmloperations.h #942639:942640
+@@ -28,6 +28,10 @@
+ #include &lt;QtCore/QObject>
+ #include &lt;QtCore/QTextStream>
++#include &lt;boost/bind.hpp>
++#include &lt;algorithm>
++
++
+ /**
+   Compares a Akonadi collection sub-tree with reference data supplied in an XML file.
+ */
+@@ -49,6 +53,7 @@
+     Q_DECLARE_FLAGS( CollectionFields, CollectionField )
++    void setCollectionKey( CollectionField field );
+     void ignoreCollectionField( CollectionField field );
+   public slots:
+@@ -59,6 +64,7 @@
+     Akonadi::Item getItemByRemoteId(const QString&amp; rid);
+     Akonadi::Collection getCollectionByRemoteId(const QString&amp; rid);
++    void setCollectionKey( const QString &amp;fieldName );
+     void ignoreCollectionField( const QString &amp;fieldName );
+     bool compare();
+@@ -78,16 +84,25 @@
+     template &lt;typename T> bool compareValue( const Akonadi::Collection &amp;col, const Akonadi::Collection &amp;refCol,
+                                              T (Akonadi::Collection::*property)() const,
+                                              CollectionField propertyType );
++    template &lt;typename T> bool compareValue( const Akonadi::Collection &amp;col, const Akonadi::Collection &amp;refCol,
++                                             T (Akonadi::Entity::*property)() const,
++                                             CollectionField propertyType );
+     template &lt;typename T> bool compareValue( const Akonadi::Item&amp; item, const Akonadi::Item&amp; refItem,
+                                              T (Akonadi::Item::*property)() const,
+                                              const char* propertyName );
+     template &lt;typename T> bool compareValue( const T&amp; value, const T&amp; refValue );
++    template &lt;typename T> void sortCollectionList( Akonadi::Collection::List &amp;list,
++                                                   T ( Akonadi::Collection::*property)() const ) const;
++    template &lt;typename T> void sortCollectionList( Akonadi::Collection::List &amp;list,
++                                                   T ( Akonadi::Entity::*property)() const ) const;
++
+   private:
+     Akonadi::Collection::List mRoots;
+     Akonadi::XmlDocument mDocument;
+     QString mErrorMsg;
+     CollectionFields mCollectionFields;
++    CollectionField mCollectionKey;
+ };
+@@ -109,6 +124,23 @@
+ }
+ template &lt;typename T>
++bool XmlOperations::compareValue( const Akonadi::Collection&amp; col, const Akonadi::Collection&amp; refCol,
++                                  T (Akonadi::Entity::*property)() const,
++                                  CollectionField propertyType )
++{
++  if ( mCollectionFields &amp; propertyType ) {
++    const bool result = compareValue&lt;T>( ((col).*(property))(), ((refCol).*(property))() );
++    if ( !result ) {
++      const QMetaEnum me = metaObject()->enumerator( metaObject()->indexOfEnumerator( "CollectionField" ) );
++      mErrorMsg.prepend( QString::fromLatin1( "Collection with remote id '%1' differs in property '%2':\n" )
++      .arg( col.remoteId() ).arg( me.valueToKey( propertyType ) ) );
++    }
++    return result;
++  }
++  return true;
++}
++
++template &lt;typename T>
+ bool XmlOperations::compareValue( const Akonadi::Item&amp; item, const Akonadi::Item&amp; refItem,
+                                   T (Akonadi::Item::*property)() const,
+                                   const char* propertyName )
+@@ -131,4 +163,18 @@
+   return false;
+ }
++template &lt;typename T>
++void XmlOperations::sortCollectionList( Akonadi::Collection::List &amp;list,
++                                        T ( Akonadi::Collection::*property)() const ) const
++{
++  std::sort( list.begin(), list.end(), boost::bind( property, _1 ) &lt; boost::bind( property, _2 ) );
++}
++
++template &lt;typename T>
++void XmlOperations::sortCollectionList( Akonadi::Collection::List &amp;list,
++                                        T ( Akonadi::Entity::*property)() const ) const
++{
++  std::sort( list.begin(), list.end(), boost::bind( property, _1 ) &lt; boost::bind( property, _2 ) );
++}
++
+ #endif
+</payload>
+        </item>
+      </collection>
+      <item mimetype="message/rfc822" rid="1237726858.6570.dtdn4!2,S" >
+        <payload>Return-Path: &lt;commitfilter@new.kstuff.org>
+Received: from localhost (localhost [127.0.0.1])
+        by smykowski.kdab.net (Cyrus v2.2.12) with LMTPA;
+        Sun, 22 Mar 2009 12:55:23 +0100
+X-Sieve: CMU Sieve 2.2
+Received: from localhost (localhost [127.0.0.1])
+       by smykowski.kdab.net (Postfix) with ESMTP id EF869E6C77A
+       for &lt;asok@kdab.net>; Sun, 22 Mar 2009 12:55:22 +0100 (CET)
+Received: from smykowski.kdab.net ([127.0.0.1])
+ by localhost (smykowski.kdab.net [127.0.0.1]) (amavisd-new, port 10024)
+ with ESMTP id 06346-10 for &lt;asok@kdab.net>;
+ Sun, 22 Mar 2009 12:55:21 +0100 (CET)
+Received: from localhost (localhost [127.0.0.1])
+       by smykowski.kdab.net (Postfix) with ESMTP id 24127E6C79E
+       for &lt;asok@kdab.net>; Sun, 22 Mar 2009 12:55:21 +0100 (CET)
+Received: from kdeget.osuosl.org (kdeget.osuosl.org [140.211.166.77])
+       by smykowski.kdab.net (Postfix) with ESMTP id D175FE6C77A
+       for &lt;asok@kdab.net>; Sun, 22 Mar 2009 12:55:20 +0100 (CET)
+Received: from ktown.kde.org ([131.246.120.250])
+       by kdeget.osuosl.org with smtp (Exim 4.63)
+       (envelope-from &lt;kde-commits-bounces-+commitfilter=new.kstuff.org@kde.org>)
+       id 1LlMMP-0003EH-9D
+       for commitfilter@new.kstuff.org; Sun, 22 Mar 2009 13:01:02 +0100
+Received: (qmail 14097 invoked by uid 72); 22 Mar 2009 12:00:55 -0000
+Received: (qmail 14075 invoked from network); 22 Mar 2009 12:00:53 -0000
+Received: from unknown (HELO office.kde.org) (195.135.221.67)
+       by ktown.kde.org with SMTP; 22 Mar 2009 12:00:51 -0000
+Received: from svn.kde.org (localhost [127.0.0.1])
+       by office.kde.org (Postfix) with SMTP id 0F54D18E
+       for &lt;kde-commits@kde.org>; Sun, 22 Mar 2009 13:00:53 +0100 (CET)
+Received: (nullmailer pid 17237 invoked by uid 30);
+       Sun, 22 Mar 2009 12:00:53 -0000
+From: Volker Krause &lt;vkrause@kde.org>
+To: kde-commits@kde.org
+Subject: playground/pim/akonaditest/resourcetester
+X-Commit-Directories: (0) trunk/playground/pim/akonaditest/resourcetester
+       trunk/playground/pim/akonaditest/resourcetester/tests
+MIME-Version: 1.0
+Content-Type: text/plain;
+  charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Date: Sun, 22 Mar 2009 12:00:53 +0000
+Message-Id: &lt;1237723253.005953.17235.nullmailer@svn.kde.org>
+X-BeenThere: kde-commits@kde.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+Reply-To: kde-commits@kde.org
+List-Id: Notification of KDE commits &lt;kde-commits.kde.org>
+List-Unsubscribe: &lt;https://mail.kde.org/mailman/listinfo/kde-commits>,
+       &lt;mailto:kde-commits-request@kde.org?subject=unsubscribe>
+List-Post: &lt;mailto:kde-commits@kde.org>
+List-Help: &lt;mailto:kde-commits-request@kde.org?subject=help>
+List-Subscribe: &lt;https://mail.kde.org/mailman/listinfo/kde-commits>,
+       &lt;mailto:kde-commits-request@kde.org?subject=subscribe>
+X-Virus-Scanned: by amavisd-new at kdab.net
+X-Kolab-Scheduling-Message: FALSE
+X-UID: 26667
+X-Length: 4226
+Status: RO
+X-Status: ORC
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+SVN commit 942650 by vkrause:
+
+Add CMake macro to run resource tests.
+
+
+ M  +20 -0     CMakeLists.txt  
+ A             tests/CMakeLists.txt  
+ AM            tests/vcardtest-readonly.js   tests/vcardtest.js#942640
+ AM            tests/vcardtest-readonly.xml   tests/vcardtest.xml#942640
+
+
+--- trunk/playground/pim/akonaditest/resourcetester/CMakeLists.txt #942649:942650
+@@ -17,6 +17,26 @@
+ set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${KDE4_ENABLE_EXCEPTIONS}" )
++macro( akonadi_add_resourcetest _testname _script )
++  if ( ${EXECUTABLE_OUTPUT_PATH} )
++    set( _exepath ${EXECUTABLE_OUTPUT_PATH} )
++  else ( ${EXECUTABLE_OUTPUT_PATH} )
++    set( _exepath ${CMAKE_CURRENT_BINARY_DIR}/.. )
++  endif ( ${EXECUTABLE_OUTPUT_PATH} )
++  if (WIN32)
++    set(_resourcetester ${_exepath}/resourcetester.bat)
++  else (WIN32)
++    set(_resourcetester ${_exepath}/resourcetester)
++  endif (WIN32)
++  if (UNIX)
++    set(_resourcetester ${_resourcetester}.shell)
++  endif (UNIX)
++
++  add_test( ${_testname} ${_resourcetester} -c ${CMAKE_CURRENT_SOURCE_DIR}/${_script} )
++endmacro( akonadi_add_resourcetest )
++
++add_subdirectory( tests )
++
+ set( resourcetester_SRCS
+   global.cpp
+   main.cpp
+** trunk/playground/pim/akonaditest/resourcetester/tests/vcardtest-readonly.js #property svn:mergeinfo
+   + 
+** trunk/playground/pim/akonaditest/resourcetester/tests/vcardtest-readonly.xml #property svn:mergeinfo
+   + 
+</payload>
+      </item>
+      <item mimetype="message/rfc822" rid="1237726875.6570.R4KOW!2,S" >
+        <payload>Return-Path: &lt;commitfilter@new.kstuff.org>
+Received: from localhost (localhost [127.0.0.1])
+        by smykowski.kdab.net (Cyrus v2.2.12) with LMTPA;
+        Sun, 22 Mar 2009 13:16:01 +0100
+X-Sieve: CMU Sieve 2.2
+Received: from localhost (localhost [127.0.0.1])
+       by smykowski.kdab.net (Postfix) with ESMTP id CA2E8E6C783
+       for &lt;asok@kdab.net>; Sun, 22 Mar 2009 13:16:00 +0100 (CET)
+Received: from smykowski.kdab.net ([127.0.0.1])
+ by localhost (smykowski.kdab.net [127.0.0.1]) (amavisd-new, port 10024)
+ with ESMTP id 07855-05 for &lt;asok@kdab.net>;
+ Sun, 22 Mar 2009 13:15:58 +0100 (CET)
+Received: from localhost (localhost [127.0.0.1])
+       by smykowski.kdab.net (Postfix) with ESMTP id ABDFDE6C79B
+       for &lt;asok@kdab.net>; Sun, 22 Mar 2009 13:15:58 +0100 (CET)
+Received: from kdeget.osuosl.org (kdeget.osuosl.org [140.211.166.77])
+       by smykowski.kdab.net (Postfix) with ESMTP id 5CD44E6C783
+       for &lt;asok@kdab.net>; Sun, 22 Mar 2009 13:15:58 +0100 (CET)
+Received: from ktown.kde.org ([131.246.120.250])
+       by kdeget.osuosl.org with smtp (Exim 4.63)
+       (envelope-from &lt;kde-commits-bounces-+commitfilter=new.kstuff.org@kde.org>)
+       id 1LlMgP-00046D-6T
+       for commitfilter@new.kstuff.org; Sun, 22 Mar 2009 13:21:41 +0100
+Received: (qmail 27078 invoked by uid 72); 22 Mar 2009 12:21:36 -0000
+Received: (qmail 27060 invoked from network); 22 Mar 2009 12:21:34 -0000
+Received: from unknown (HELO office.kde.org) (195.135.221.67)
+       by ktown.kde.org with SMTP; 22 Mar 2009 12:21:32 -0000
+Received: from svn.kde.org (localhost [127.0.0.1])
+       by office.kde.org (Postfix) with SMTP id 0A38E18E
+       for &lt;kde-commits@kde.org>; Sun, 22 Mar 2009 13:21:34 +0100 (CET)
+Received: (nullmailer pid 20237 invoked by uid 30);
+       Sun, 22 Mar 2009 12:21:34 -0000
+From: Volker Krause &lt;vkrause@kde.org>
+To: kde-commits@kde.org
+Subject: playground/pim/akonaditest/resourcetester
+X-Commit-Directories: (0) trunk/playground/pim/akonaditest/resourcetester
+MIME-Version: 1.0
+Content-Type: text/plain;
+  charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Date: Sun, 22 Mar 2009 12:21:34 +0000
+Message-Id: &lt;1237724494.009832.20236.nullmailer@svn.kde.org>
+X-BeenThere: kde-commits@kde.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+Reply-To: kde-commits@kde.org
+List-Id: Notification of KDE commits &lt;kde-commits.kde.org>
+List-Unsubscribe: &lt;https://mail.kde.org/mailman/listinfo/kde-commits>,
+       &lt;mailto:kde-commits-request@kde.org?subject=unsubscribe>
+List-Post: &lt;mailto:kde-commits@kde.org>
+List-Help: &lt;mailto:kde-commits-request@kde.org?subject=help>
+List-Subscribe: &lt;https://mail.kde.org/mailman/listinfo/kde-commits>,
+       &lt;mailto:kde-commits-request@kde.org?subject=subscribe>
+X-Virus-Scanned: by amavisd-new at kdab.net
+X-Kolab-Scheduling-Message: FALSE
+X-UID: 26668
+X-Length: 4765
+Status: RO
+X-Status: ORC
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+SVN commit 942656 by vkrause:
+
+- propagate script errors
+- make sure the Akonadi server is operational
+
+
+ M  +5 -0      main.cpp  
+ M  +13 -1     script.cpp  
+ M  +6 -3      script.h  
+
+
+--- trunk/playground/pim/akonaditest/resourcetester/main.cpp #942655:942656
+@@ -21,6 +21,8 @@
+ #include "global.h"
+ #include "test.h"
++#include &lt;akonadi/control.h>
++
+ #include &lt;KApplication>
+ #include &lt;KAboutData>
+ #include &lt;KCmdLineArgs>
+@@ -71,6 +73,9 @@
+   signal( SIGQUIT, sigHandler );
+ #endif
++  if ( !Akonadi::Control::start() )
++    qFatal( "Unable to start Akonadi!" );
++
+   Script *script = new Script();
+   script->configure(path);
+--- trunk/playground/pim/akonaditest/resourcetester/script.cpp #942655:942656
+@@ -16,12 +16,13 @@
+  */
+ #include "script.h"
++#include &lt;KDebug>
+ #include &lt;qcoreapplication.h>
+ Script::Script()
+ {
+   action = new Kross::Action(this, "ResourceTester");
+-  connect( action, SIGNAL(finished(Kross::Action*)), QCoreApplication::instance(), SLOT(quit()) );
++  connect( action, SIGNAL(finished(Kross::Action*)), SLOT(finished(Kross::Action*)) );
+ }
+ void Script::configure(const QString &amp;path, QHash&lt;QString, QObject * > hash)
+@@ -51,4 +52,15 @@
+   action->trigger();
+ }
++void Script::finished(Kross::Action* action)
++{
++  if ( action->hadError() ) {
++    kError() &lt;&lt; action->errorMessage() &lt;&lt; action->errorTrace();
++    QCoreApplication::instance()->exit( 1 );
++  } else {
++    QCoreApplication::instance()->quit();
++  }
++}
++
++
+ #include "script.moc"
+--- trunk/playground/pim/akonaditest/resourcetester/script.h #942655:942656
+@@ -24,9 +24,6 @@
+ class Script : public QObject
+ {
+   Q_OBJECT
+-  private:
+-    Kross::Action *action;
+-
+   public:
+     Script();
+     void configure(const QString &amp;path, QHash&lt;QString, QObject *> hash);
+@@ -35,6 +32,12 @@
+   public slots:
+     void start();
++
++  private slots:
++    void finished( Kross::Action *action );
++
++  private:
++    Kross::Action *action;
+ };
+ #endif
+</payload>
+      </item>
+    </collection>
+    <item mimetype="message/rfc822" rid="1237726845.6570.BejQg!2,S" >
+      <payload>Return-Path: &lt;commitfilter@new.kstuff.org>
+Received: from localhost (localhost [127.0.0.1])
+        by smykowski.kdab.net (Cyrus v2.2.12) with LMTPA;
+        Sun, 22 Mar 2009 13:45:00 +0100
+X-Sieve: CMU Sieve 2.2
+Received: from localhost (localhost [127.0.0.1])
+       by smykowski.kdab.net (Postfix) with ESMTP id 8C4FFE6C79B
+       for &lt;asok@kdab.net>; Sun, 22 Mar 2009 13:45:00 +0100 (CET)
+Received: from smykowski.kdab.net ([127.0.0.1])
+ by localhost (smykowski.kdab.net [127.0.0.1]) (amavisd-new, port 10024)
+ with ESMTP id 09703-05 for &lt;asok@kdab.net>;
+ Sun, 22 Mar 2009 13:45:00 +0100 (CET)
+Received: from localhost (localhost [127.0.0.1])
+       by smykowski.kdab.net (Postfix) with ESMTP id 163D2E6C7AF
+       for &lt;asok@kdab.net>; Sun, 22 Mar 2009 13:45:00 +0100 (CET)
+Received: from kdeget.osuosl.org (kdeget.osuosl.org [140.211.166.77])
+       by smykowski.kdab.net (Postfix) with ESMTP id 31945E6C79E
+       for &lt;asok@kdab.net>; Sun, 22 Mar 2009 13:44:59 +0100 (CET)
+Received: from ktown.kde.org ([131.246.120.250])
+       by kdeget.osuosl.org with smtp (Exim 4.63)
+       (envelope-from &lt;kde-commits-bounces-+commitfilter=new.kstuff.org@kde.org>)
+       id 1LlN8R-0005Jr-VE
+       for commitfilter@new.kstuff.org; Sun, 22 Mar 2009 13:50:40 +0100
+Received: (qmail 7667 invoked by uid 72); 22 Mar 2009 12:50:33 -0000
+Received: (qmail 7658 invoked from network); 22 Mar 2009 12:50:31 -0000
+Received: from unknown (HELO office.kde.org) (195.135.221.67)
+       by ktown.kde.org with SMTP; 22 Mar 2009 12:50:29 -0000
+Received: from svn.kde.org (localhost [127.0.0.1])
+       by office.kde.org (Postfix) with SMTP id 93E9F18E
+       for &lt;kde-commits@kde.org>; Sun, 22 Mar 2009 13:50:30 +0100 (CET)
+Received: (nullmailer pid 25707 invoked by uid 30);
+       Sun, 22 Mar 2009 12:50:30 -0000
+From: Volker Krause &lt;vkrause@kde.org>
+To: kde-commits@kde.org
+Subject: playground/pim/akonaditest/resourcetester
+X-Commit-Directories: (0) trunk/playground/pim/akonaditest/resourcetester
+MIME-Version: 1.0
+Content-Type: text/plain;
+  charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Date: Sun, 22 Mar 2009 12:50:30 +0000
+Message-Id: &lt;1237726230.394911.25706.nullmailer@svn.kde.org>
+X-BeenThere: kde-commits@kde.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+Reply-To: kde-commits@kde.org
+List-Id: Notification of KDE commits &lt;kde-commits.kde.org>
+List-Unsubscribe: &lt;https://mail.kde.org/mailman/listinfo/kde-commits>,
+       &lt;mailto:kde-commits-request@kde.org?subject=unsubscribe>
+List-Post: &lt;mailto:kde-commits@kde.org>
+List-Help: &lt;mailto:kde-commits-request@kde.org?subject=help>
+List-Subscribe: &lt;https://mail.kde.org/mailman/listinfo/kde-commits>,
+       &lt;mailto:kde-commits-request@kde.org?subject=subscribe>
+X-Virus-Scanned: by amavisd-new at kdab.net
+X-Kolab-Scheduling-Message: FALSE
+X-UID: 26669
+X-Length: 5694
+Status: RO
+X-Status: RC
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+SVN commit 942677 by vkrause:
+
+Add a safety timeout in case we do not receive the synchronized() signal
+or the resource hangs during syncing. The first seems to happen randomly
+if syncing is extremely fast.
+
+
+ M  +40 -0     resourcesynchronizationjob.cpp  
+ M  +1 -1      resourcesynchronizationjob.h  
+
+
+--- trunk/playground/pim/akonaditest/resourcetester/resourcesynchronizationjob.cpp #942676:942677
+@@ -18,12 +18,14 @@
+ #include "resourcesynchronizationjob.h"
+ #include &lt;akonadi/agentinstance.h>
++#include &lt;akonadi/agentmanager.h>
+ #include &lt;KDebug>
+ #include &lt;KLocale>
+ #include &lt;QDBusConnection>
+ #include &lt;QDBusInterface>
++#include &lt;QTimer>
+ namespace Akonadi
+ {
+@@ -31,15 +33,31 @@
+ class ResourceSynchronizationJobPrivate
+ {
+   public:
++    ResourceSynchronizationJobPrivate() :
++      interface( 0 ),
++      safetyTimer( 0 ),
++      timeoutCount( 0 )
++    {}
++
+     AgentInstance instance;
+     QDBusInterface* interface;
++    QTimer* safetyTimer;
++    int timeoutCount;
++    static int timeoutCountLimit;
+ };
++int ResourceSynchronizationJobPrivate::timeoutCountLimit = 60;
++
+ ResourceSynchronizationJob::ResourceSynchronizationJob(const AgentInstance&amp; instance, QObject* parent) :
+   KJob( parent ),
+   d( new ResourceSynchronizationJobPrivate )
+ {
+   d->instance = instance;
++  d->safetyTimer = new QTimer( this );
++  connect( d->safetyTimer, SIGNAL(timeout()), SLOT(slotTimeout()) );
++  d->safetyTimer->setInterval( 10 * 1000 );
++  d->safetyTimer->setSingleShot( false );
++  d->safetyTimer->start();
+ }
+ ResourceSynchronizationJob::~ResourceSynchronizationJob()
+@@ -72,9 +90,31 @@
+ void ResourceSynchronizationJob::slotSynchronized()
+ {
++  disconnect( d->interface, SIGNAL(synchronized()), this, SLOT(slotSynchronized()) );
++  d->safetyTimer->stop();
+   emitResult();
+ }
++void ResourceSynchronizationJob::slotTimeout()
++{
++  d->instance = AgentManager::self()->instance( d->instance.identifier() );
++  d->timeoutCount++;
++
++  if ( d->timeoutCount > d->timeoutCountLimit ) {
++    d->safetyTimer->stop();
++    setError( UserDefinedError );
++    setErrorText( i18n( "Resource synchronization timed out." ) );
++    emitResult();
++    return;
++  }
++
++  if ( d->instance.status() == AgentInstance::Idle ) {
++    // try again, we might have lost the synchronized() signal
++    kDebug() &lt;&lt; "trying again to sync resource" &lt;&lt; d->instance.identifier();
++    d->instance.synchronize();
++  }
+ }
++}
++
+ #include "resourcesynchronizationjob.moc"
+--- trunk/playground/pim/akonaditest/resourcetester/resourcesynchronizationjob.h #942676:942677
+@@ -27,7 +27,6 @@
+ /**
+   Synchronizes a given resource.
+-  @todo Add safety timeouts.
+ */
+ class ResourceSynchronizationJob : public KJob
+ {
+@@ -48,6 +47,7 @@
+   private slots:
+     void slotSynchronized();
++    void slotTimeout();
+   private:
+     ResourceSynchronizationJobPrivate* const d;
+</payload>
+    </item>
+  </collection>
+</knut>
diff --git a/resources/maildir/autotests/maildir/.root.directory/.child1.directory/grandchild/cur/1237726881.6570.rfoxg!2,S b/resources/maildir/autotests/maildir/.root.directory/.child1.directory/grandchild/cur/1237726881.6570.rfoxg!2,S
new file mode 100644 (file)
index 0000000..382f2ad
--- /dev/null
@@ -0,0 +1,282 @@
+Return-Path: <commitfilter@new.kstuff.org>
+Received: from localhost (localhost [127.0.0.1])
+        by smykowski.kdab.net (Cyrus v2.2.12) with LMTPA;
+        Sun, 22 Mar 2009 12:10:48 +0100
+X-Sieve: CMU Sieve 2.2
+Received: from localhost (localhost [127.0.0.1])
+       by smykowski.kdab.net (Postfix) with ESMTP id 4BDF8E6C790
+       for <asok@kdab.net>; Sun, 22 Mar 2009 12:10:48 +0100 (CET)
+Received: from smykowski.kdab.net ([127.0.0.1])
+ by localhost (smykowski.kdab.net [127.0.0.1]) (amavisd-new, port 10024)
+ with ESMTP id 03694-02 for <asok@kdab.net>;
+ Sun, 22 Mar 2009 12:10:45 +0100 (CET)
+Received: from localhost (localhost [127.0.0.1])
+       by smykowski.kdab.net (Postfix) with ESMTP id B289FE6C79B
+       for <asok@kdab.net>; Sun, 22 Mar 2009 12:10:45 +0100 (CET)
+Received: from kdeget.osuosl.org (kdeget.osuosl.org [140.211.166.77])
+       by smykowski.kdab.net (Postfix) with ESMTP id 3B465E6C790
+       for <asok@kdab.net>; Sun, 22 Mar 2009 12:10:45 +0100 (CET)
+Received: from ktown.kde.org ([131.246.120.250])
+       by kdeget.osuosl.org with smtp (Exim 4.63)
+       (envelope-from <kde-commits-bounces-+commitfilter=new.kstuff.org@kde.org>)
+       id 1LlLfE-0001OT-K7
+       for commitfilter@new.kstuff.org; Sun, 22 Mar 2009 12:16:25 +0100
+Received: (qmail 23006 invoked by uid 72); 22 Mar 2009 11:16:19 -0000
+Received: (qmail 22986 invoked from network); 22 Mar 2009 11:16:14 -0000
+Received: from unknown (HELO office.kde.org) (195.135.221.67)
+       by ktown.kde.org with SMTP; 22 Mar 2009 11:16:11 -0000
+Received: from svn.kde.org (localhost [127.0.0.1])
+       by office.kde.org (Postfix) with SMTP id 85EE718E
+       for <kde-commits@kde.org>; Sun, 22 Mar 2009 12:16:12 +0100 (CET)
+Received: (nullmailer pid 13467 invoked by uid 30);
+       Sun, 22 Mar 2009 11:16:12 -0000
+From: Volker Krause <vkrause@kde.org>
+To: kde-commits@kde.org
+Subject: playground/pim/akonaditest/resourcetester
+X-Commit-Directories: (0) trunk/playground/pim/akonaditest/resourcetester
+       trunk/playground/pim/akonaditest/resourcetester/tests
+MIME-Version: 1.0
+Content-Type: text/plain;
+  charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Date: Sun, 22 Mar 2009 11:16:12 +0000
+Message-Id: <1237720572.493438.13466.nullmailer@svn.kde.org>
+X-BeenThere: kde-commits@kde.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+Reply-To: kde-commits@kde.org
+List-Id: Notification of KDE commits <kde-commits.kde.org>
+List-Unsubscribe: <https://mail.kde.org/mailman/listinfo/kde-commits>,
+       <mailto:kde-commits-request@kde.org?subject=unsubscribe>
+List-Post: <mailto:kde-commits@kde.org>
+List-Help: <mailto:kde-commits-request@kde.org?subject=help>
+List-Subscribe: <https://mail.kde.org/mailman/listinfo/kde-commits>,
+       <mailto:kde-commits-request@kde.org?subject=subscribe>
+X-Virus-Scanned: by amavisd-new at kdab.net
+X-Kolab-Scheduling-Message: FALSE
+X-UID: 26666
+X-Length: 11240
+Status: RO
+X-Status: ORC
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+SVN commit 942640 by vkrause:
+
+Allow to specifiy the collection property used to identify corresponding
+collections.
+
+
+ M  +2 -0      tests/vcardtest.js  
+ M  +1 -1      tests/vcardtest.xml  
+ M  +29 -14    xmloperations.cpp  
+ M  +46 -0     xmloperations.h  
+
+
+--- trunk/playground/pim/akonaditest/resourcetester/tests/vcardtest.js #942639:942640
+@@ -4,6 +4,8 @@
+ XmlOperations.setXmlFile( "vcardtest.xml" );
+ XmlOperations.setRootCollections( Resource.identifier() );
++XmlOperations.setCollectionKey( "None" ); // we only expect one collection
+ XmlOperations.ignoreCollectionField( "Name" ); // name is the resource identifier and thus unpredictable
++XmlOperations.ignoreCollectionField( "RemoteId" ); // remote id is the absolute path
+ XmlOperations.assertEqual();
+--- trunk/playground/pim/akonaditest/resourcetester/tests/vcardtest.xml #942639:942640
+@@ -1,5 +1,5 @@
+ <knut>
+-  <collection rid="/k/kde4/src/playground/pim/akonaditest/resourcetester/tests/vcardtest.vcf" name="akonadi_vcard_resource_0" content="text/directory">
++  <collection rid="vcardtest.vcf" name="akonadi_vcard_resource_0" content="text/directory">
+     <attribute type="AccessRights" >wcdW</attribute>
+     <attribute type="ENTITYDISPLAY" >("vcardtest.vcf" "office-address-book")</attribute>
+     <item rid="bb2slGmqxb" mimetype="text/directory">
+--- trunk/playground/pim/akonaditest/resourcetester/xmloperations.cpp #942639:942640
+@@ -31,9 +31,6 @@
+ #include <QFileInfo>
+ #include <QStringList>
+-#include <boost/bind.hpp>
+-#include <algorithm>
+-
+ using namespace Akonadi;
+ template <typename T> QTextStream& operator<<( QTextStream &s, const QSet<T> &set )
+@@ -53,7 +50,8 @@
+ XmlOperations::XmlOperations(QObject* parent) :
+   QObject( parent ),
+-  mCollectionFields( 0xFF )
++  mCollectionFields( 0xFF ),
++  mCollectionKey( RemoteId )
+ {
+ }
+@@ -99,6 +97,17 @@
+   return mErrorMsg;
+ }
++void XmlOperations::setCollectionKey(XmlOperations::CollectionField field)
++{
++  mCollectionKey = field;
++}
++
++void XmlOperations::setCollectionKey(const QString& fieldName)
++{
++  const QMetaEnum me = metaObject()->enumerator( metaObject()->indexOfEnumerator( "CollectionField" ) );
++  setCollectionKey( static_cast<CollectionField>( me.keyToValue( fieldName.toLatin1() ) ) );
++}
++
+ void XmlOperations::ignoreCollectionField(XmlOperations::CollectionField field)
+ {
+   mCollectionFields = mCollectionFields & ~field;
+@@ -137,8 +146,20 @@
+ {
+   Collection::List cols( _cols );
+   Collection::List refCols( _refCols );
+-  std::sort( cols.begin(), cols.end(), boost::bind( &Collection::remoteId, _1 ) < boost::bind( &Collection::remoteId, _2 ) );
+-  std::sort( refCols.begin(), refCols.end(), boost::bind( &Collection::remoteId, _1 ) < boost::bind( &Collection::remoteId, _2 ) );
++  switch ( mCollectionKey ) {
++    case RemoteId:
++      sortCollectionList( cols, &Collection::remoteId );
++      sortCollectionList( refCols, &Collection::remoteId );
++      break;
++    case Name:
++      sortCollectionList( cols, &Collection::name );
++      sortCollectionList( refCols, &Collection::name );
++      break;
++    case None:
++      break;
++    default:
++      Q_ASSERT( false );
++  }
+   for ( int i = 0; i < cols.count(); ++i ) {
+     const Collection col = cols.at( i );
+@@ -148,11 +169,6 @@
+     }
+     const Collection refCol = refCols.at( i );
+-    if ( col.remoteId() != refCol.remoteId() ) {
+-      mErrorMsg = QString::fromLatin1( "Collection with remote id '%1' is missing." ).arg( refCol.remoteId() );
+-      return false;
+-    }
+-
+     if ( !compareCollection( col, refCol ) )
+       return false;
+   }
+@@ -177,14 +193,13 @@
+ bool XmlOperations::compareCollection(const Collection& _col, const Collection& _refCol)
+ {
+-  Q_ASSERT( _col.remoteId() == _refCol.remoteId() );
+-
+   // normalize
+   Collection col( normalize( _col ) );
+   Collection refCol( normalize( _refCol ) );
+   
+   // compare the two collections
+-  if ( !compareValue( col, refCol, &Collection::contentMimeTypes, ContentMimeType ) ||
++  if ( !compareValue( col, refCol, &Collection::remoteId, RemoteId ) ||
++       !compareValue( col, refCol, &Collection::contentMimeTypes, ContentMimeType ) ||
+        !compareValue( col, refCol, &Collection::name, Name ) )
+     return false;
+--- trunk/playground/pim/akonaditest/resourcetester/xmloperations.h #942639:942640
+@@ -28,6 +28,10 @@
+ #include <QtCore/QObject>
+ #include <QtCore/QTextStream>
++#include <boost/bind.hpp>
++#include <algorithm>
++
++
+ /**
+   Compares a Akonadi collection sub-tree with reference data supplied in an XML file.
+ */
+@@ -49,6 +53,7 @@
+     Q_DECLARE_FLAGS( CollectionFields, CollectionField )
++    void setCollectionKey( CollectionField field );
+     void ignoreCollectionField( CollectionField field );
+   public slots:
+@@ -59,6 +64,7 @@
+     Akonadi::Item getItemByRemoteId(const QString& rid);
+     Akonadi::Collection getCollectionByRemoteId(const QString& rid);
++    void setCollectionKey( const QString &fieldName );
+     void ignoreCollectionField( const QString &fieldName );
+     bool compare();
+@@ -78,16 +84,25 @@
+     template <typename T> bool compareValue( const Akonadi::Collection &col, const Akonadi::Collection &refCol,
+                                              T (Akonadi::Collection::*property)() const,
+                                              CollectionField propertyType );
++    template <typename T> bool compareValue( const Akonadi::Collection &col, const Akonadi::Collection &refCol,
++                                             T (Akonadi::Entity::*property)() const,
++                                             CollectionField propertyType );
+     template <typename T> bool compareValue( const Akonadi::Item& item, const Akonadi::Item& refItem,
+                                              T (Akonadi::Item::*property)() const,
+                                              const char* propertyName );
+     template <typename T> bool compareValue( const T& value, const T& refValue );
++    template <typename T> void sortCollectionList( Akonadi::Collection::List &list,
++                                                   T ( Akonadi::Collection::*property)() const ) const;
++    template <typename T> void sortCollectionList( Akonadi::Collection::List &list,
++                                                   T ( Akonadi::Entity::*property)() const ) const;
++
+   private:
+     Akonadi::Collection::List mRoots;
+     Akonadi::XmlDocument mDocument;
+     QString mErrorMsg;
+     CollectionFields mCollectionFields;
++    CollectionField mCollectionKey;
+ };
+@@ -109,6 +124,23 @@
+ }
+ template <typename T>
++bool XmlOperations::compareValue( const Akonadi::Collection& col, const Akonadi::Collection& refCol,
++                                  T (Akonadi::Entity::*property)() const,
++                                  CollectionField propertyType )
++{
++  if ( mCollectionFields & propertyType ) {
++    const bool result = compareValue<T>( ((col).*(property))(), ((refCol).*(property))() );
++    if ( !result ) {
++      const QMetaEnum me = metaObject()->enumerator( metaObject()->indexOfEnumerator( "CollectionField" ) );
++      mErrorMsg.prepend( QString::fromLatin1( "Collection with remote id '%1' differs in property '%2':\n" )
++      .arg( col.remoteId() ).arg( me.valueToKey( propertyType ) ) );
++    }
++    return result;
++  }
++  return true;
++}
++
++template <typename T>
+ bool XmlOperations::compareValue( const Akonadi::Item& item, const Akonadi::Item& refItem,
+                                   T (Akonadi::Item::*property)() const,
+                                   const char* propertyName )
+@@ -131,4 +163,18 @@
+   return false;
+ }
++template <typename T>
++void XmlOperations::sortCollectionList( Akonadi::Collection::List &list,
++                                        T ( Akonadi::Collection::*property)() const ) const
++{
++  std::sort( list.begin(), list.end(), boost::bind( property, _1 ) < boost::bind( property, _2 ) );
++}
++
++template <typename T>
++void XmlOperations::sortCollectionList( Akonadi::Collection::List &list,
++                                        T ( Akonadi::Entity::*property)() const ) const
++{
++  std::sort( list.begin(), list.end(), boost::bind( property, _1 ) < boost::bind( property, _2 ) );
++}
++
+ #endif
diff --git a/resources/maildir/autotests/maildir/.root.directory/.child1.directory/grandchild/new/.keep b/resources/maildir/autotests/maildir/.root.directory/.child1.directory/grandchild/new/.keep
new file mode 100644 (file)
index 0000000..2f1d07a
--- /dev/null
@@ -0,0 +1 @@
+force git to include this file
diff --git a/resources/maildir/autotests/maildir/.root.directory/.child1.directory/grandchild/tmp/.keep b/resources/maildir/autotests/maildir/.root.directory/.child1.directory/grandchild/tmp/.keep
new file mode 100644 (file)
index 0000000..2f1d07a
--- /dev/null
@@ -0,0 +1 @@
+force git to include this file
diff --git a/resources/maildir/autotests/maildir/.root.directory/child1/cur/1237726858.6570.dtdn4!2,S b/resources/maildir/autotests/maildir/.root.directory/child1/cur/1237726858.6570.dtdn4!2,S
new file mode 100644 (file)
index 0000000..b9ee984
--- /dev/null
@@ -0,0 +1,107 @@
+Return-Path: <commitfilter@new.kstuff.org>
+Received: from localhost (localhost [127.0.0.1])
+        by smykowski.kdab.net (Cyrus v2.2.12) with LMTPA;
+        Sun, 22 Mar 2009 12:55:23 +0100
+X-Sieve: CMU Sieve 2.2
+Received: from localhost (localhost [127.0.0.1])
+       by smykowski.kdab.net (Postfix) with ESMTP id EF869E6C77A
+       for <asok@kdab.net>; Sun, 22 Mar 2009 12:55:22 +0100 (CET)
+Received: from smykowski.kdab.net ([127.0.0.1])
+ by localhost (smykowski.kdab.net [127.0.0.1]) (amavisd-new, port 10024)
+ with ESMTP id 06346-10 for <asok@kdab.net>;
+ Sun, 22 Mar 2009 12:55:21 +0100 (CET)
+Received: from localhost (localhost [127.0.0.1])
+       by smykowski.kdab.net (Postfix) with ESMTP id 24127E6C79E
+       for <asok@kdab.net>; Sun, 22 Mar 2009 12:55:21 +0100 (CET)
+Received: from kdeget.osuosl.org (kdeget.osuosl.org [140.211.166.77])
+       by smykowski.kdab.net (Postfix) with ESMTP id D175FE6C77A
+       for <asok@kdab.net>; Sun, 22 Mar 2009 12:55:20 +0100 (CET)
+Received: from ktown.kde.org ([131.246.120.250])
+       by kdeget.osuosl.org with smtp (Exim 4.63)
+       (envelope-from <kde-commits-bounces-+commitfilter=new.kstuff.org@kde.org>)
+       id 1LlMMP-0003EH-9D
+       for commitfilter@new.kstuff.org; Sun, 22 Mar 2009 13:01:02 +0100
+Received: (qmail 14097 invoked by uid 72); 22 Mar 2009 12:00:55 -0000
+Received: (qmail 14075 invoked from network); 22 Mar 2009 12:00:53 -0000
+Received: from unknown (HELO office.kde.org) (195.135.221.67)
+       by ktown.kde.org with SMTP; 22 Mar 2009 12:00:51 -0000
+Received: from svn.kde.org (localhost [127.0.0.1])
+       by office.kde.org (Postfix) with SMTP id 0F54D18E
+       for <kde-commits@kde.org>; Sun, 22 Mar 2009 13:00:53 +0100 (CET)
+Received: (nullmailer pid 17237 invoked by uid 30);
+       Sun, 22 Mar 2009 12:00:53 -0000
+From: Volker Krause <vkrause@kde.org>
+To: kde-commits@kde.org
+Subject: playground/pim/akonaditest/resourcetester
+X-Commit-Directories: (0) trunk/playground/pim/akonaditest/resourcetester
+       trunk/playground/pim/akonaditest/resourcetester/tests
+MIME-Version: 1.0
+Content-Type: text/plain;
+  charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Date: Sun, 22 Mar 2009 12:00:53 +0000
+Message-Id: <1237723253.005953.17235.nullmailer@svn.kde.org>
+X-BeenThere: kde-commits@kde.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+Reply-To: kde-commits@kde.org
+List-Id: Notification of KDE commits <kde-commits.kde.org>
+List-Unsubscribe: <https://mail.kde.org/mailman/listinfo/kde-commits>,
+       <mailto:kde-commits-request@kde.org?subject=unsubscribe>
+List-Post: <mailto:kde-commits@kde.org>
+List-Help: <mailto:kde-commits-request@kde.org?subject=help>
+List-Subscribe: <https://mail.kde.org/mailman/listinfo/kde-commits>,
+       <mailto:kde-commits-request@kde.org?subject=subscribe>
+X-Virus-Scanned: by amavisd-new at kdab.net
+X-Kolab-Scheduling-Message: FALSE
+X-UID: 26667
+X-Length: 4226
+Status: RO
+X-Status: ORC
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+SVN commit 942650 by vkrause:
+
+Add CMake macro to run resource tests.
+
+
+ M  +20 -0     CMakeLists.txt  
+ A             tests/CMakeLists.txt  
+ AM            tests/vcardtest-readonly.js   tests/vcardtest.js#942640
+ AM            tests/vcardtest-readonly.xml   tests/vcardtest.xml#942640
+
+
+--- trunk/playground/pim/akonaditest/resourcetester/CMakeLists.txt #942649:942650
+@@ -17,6 +17,26 @@
+ set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${KDE4_ENABLE_EXCEPTIONS}" )
++macro( akonadi_add_resourcetest _testname _script )
++  if ( ${EXECUTABLE_OUTPUT_PATH} )
++    set( _exepath ${EXECUTABLE_OUTPUT_PATH} )
++  else ( ${EXECUTABLE_OUTPUT_PATH} )
++    set( _exepath ${CMAKE_CURRENT_BINARY_DIR}/.. )
++  endif ( ${EXECUTABLE_OUTPUT_PATH} )
++  if (WIN32)
++    set(_resourcetester ${_exepath}/resourcetester.bat)
++  else (WIN32)
++    set(_resourcetester ${_exepath}/resourcetester)
++  endif (WIN32)
++  if (UNIX)
++    set(_resourcetester ${_resourcetester}.shell)
++  endif (UNIX)
++
++  add_test( ${_testname} ${_resourcetester} -c ${CMAKE_CURRENT_SOURCE_DIR}/${_script} )
++endmacro( akonadi_add_resourcetest )
++
++add_subdirectory( tests )
++
+ set( resourcetester_SRCS
+   global.cpp
+   main.cpp
+** trunk/playground/pim/akonaditest/resourcetester/tests/vcardtest-readonly.js #property svn:mergeinfo
+   + 
+** trunk/playground/pim/akonaditest/resourcetester/tests/vcardtest-readonly.xml #property svn:mergeinfo
+   + 
diff --git a/resources/maildir/autotests/maildir/.root.directory/child1/cur/1237726875.6570.R4KOW!2,S b/resources/maildir/autotests/maildir/.root.directory/child1/cur/1237726875.6570.R4KOW!2,S
new file mode 100644 (file)
index 0000000..447a267
--- /dev/null
@@ -0,0 +1,150 @@
+Return-Path: <commitfilter@new.kstuff.org>
+Received: from localhost (localhost [127.0.0.1])
+        by smykowski.kdab.net (Cyrus v2.2.12) with LMTPA;
+        Sun, 22 Mar 2009 13:16:01 +0100
+X-Sieve: CMU Sieve 2.2
+Received: from localhost (localhost [127.0.0.1])
+       by smykowski.kdab.net (Postfix) with ESMTP id CA2E8E6C783
+       for <asok@kdab.net>; Sun, 22 Mar 2009 13:16:00 +0100 (CET)
+Received: from smykowski.kdab.net ([127.0.0.1])
+ by localhost (smykowski.kdab.net [127.0.0.1]) (amavisd-new, port 10024)
+ with ESMTP id 07855-05 for <asok@kdab.net>;
+ Sun, 22 Mar 2009 13:15:58 +0100 (CET)
+Received: from localhost (localhost [127.0.0.1])
+       by smykowski.kdab.net (Postfix) with ESMTP id ABDFDE6C79B
+       for <asok@kdab.net>; Sun, 22 Mar 2009 13:15:58 +0100 (CET)
+Received: from kdeget.osuosl.org (kdeget.osuosl.org [140.211.166.77])
+       by smykowski.kdab.net (Postfix) with ESMTP id 5CD44E6C783
+       for <asok@kdab.net>; Sun, 22 Mar 2009 13:15:58 +0100 (CET)
+Received: from ktown.kde.org ([131.246.120.250])
+       by kdeget.osuosl.org with smtp (Exim 4.63)
+       (envelope-from <kde-commits-bounces-+commitfilter=new.kstuff.org@kde.org>)
+       id 1LlMgP-00046D-6T
+       for commitfilter@new.kstuff.org; Sun, 22 Mar 2009 13:21:41 +0100
+Received: (qmail 27078 invoked by uid 72); 22 Mar 2009 12:21:36 -0000
+Received: (qmail 27060 invoked from network); 22 Mar 2009 12:21:34 -0000
+Received: from unknown (HELO office.kde.org) (195.135.221.67)
+       by ktown.kde.org with SMTP; 22 Mar 2009 12:21:32 -0000
+Received: from svn.kde.org (localhost [127.0.0.1])
+       by office.kde.org (Postfix) with SMTP id 0A38E18E
+       for <kde-commits@kde.org>; Sun, 22 Mar 2009 13:21:34 +0100 (CET)
+Received: (nullmailer pid 20237 invoked by uid 30);
+       Sun, 22 Mar 2009 12:21:34 -0000
+From: Volker Krause <vkrause@kde.org>
+To: kde-commits@kde.org
+Subject: playground/pim/akonaditest/resourcetester
+X-Commit-Directories: (0) trunk/playground/pim/akonaditest/resourcetester
+MIME-Version: 1.0
+Content-Type: text/plain;
+  charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Date: Sun, 22 Mar 2009 12:21:34 +0000
+Message-Id: <1237724494.009832.20236.nullmailer@svn.kde.org>
+X-BeenThere: kde-commits@kde.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+Reply-To: kde-commits@kde.org
+List-Id: Notification of KDE commits <kde-commits.kde.org>
+List-Unsubscribe: <https://mail.kde.org/mailman/listinfo/kde-commits>,
+       <mailto:kde-commits-request@kde.org?subject=unsubscribe>
+List-Post: <mailto:kde-commits@kde.org>
+List-Help: <mailto:kde-commits-request@kde.org?subject=help>
+List-Subscribe: <https://mail.kde.org/mailman/listinfo/kde-commits>,
+       <mailto:kde-commits-request@kde.org?subject=subscribe>
+X-Virus-Scanned: by amavisd-new at kdab.net
+X-Kolab-Scheduling-Message: FALSE
+X-UID: 26668
+X-Length: 4765
+Status: RO
+X-Status: ORC
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+SVN commit 942656 by vkrause:
+
+- propagate script errors
+- make sure the Akonadi server is operational
+
+
+ M  +5 -0      main.cpp  
+ M  +13 -1     script.cpp  
+ M  +6 -3      script.h  
+
+
+--- trunk/playground/pim/akonaditest/resourcetester/main.cpp #942655:942656
+@@ -21,6 +21,8 @@
+ #include "global.h"
+ #include "test.h"
++#include <akonadi/control.h>
++
+ #include <KApplication>
+ #include <KAboutData>
+ #include <KCmdLineArgs>
+@@ -71,6 +73,9 @@
+   signal( SIGQUIT, sigHandler );
+ #endif
++  if ( !Akonadi::Control::start() )
++    qFatal( "Unable to start Akonadi!" );
++
+   Script *script = new Script();
+   script->configure(path);
+--- trunk/playground/pim/akonaditest/resourcetester/script.cpp #942655:942656
+@@ -16,12 +16,13 @@
+  */
+ #include "script.h"
++#include <KDebug>
+ #include <qcoreapplication.h>
+ Script::Script()
+ {
+   action = new Kross::Action(this, "ResourceTester");
+-  connect( action, SIGNAL(finished(Kross::Action*)), QCoreApplication::instance(), SLOT(quit()) );
++  connect( action, SIGNAL(finished(Kross::Action*)), SLOT(finished(Kross::Action*)) );
+ }
+ void Script::configure(const QString &path, QHash<QString, QObject * > hash)
+@@ -51,4 +52,15 @@
+   action->trigger();
+ }
++void Script::finished(Kross::Action* action)
++{
++  if ( action->hadError() ) {
++    kError() << action->errorMessage() << action->errorTrace();
++    QCoreApplication::instance()->exit( 1 );
++  } else {
++    QCoreApplication::instance()->quit();
++  }
++}
++
++
+ #include "script.moc"
+--- trunk/playground/pim/akonaditest/resourcetester/script.h #942655:942656
+@@ -24,9 +24,6 @@
+ class Script : public QObject
+ {
+   Q_OBJECT
+-  private:
+-    Kross::Action *action;
+-
+   public:
+     Script();
+     void configure(const QString &path, QHash<QString, QObject *> hash);
+@@ -35,6 +32,12 @@
+   public slots:
+     void start();
++
++  private slots:
++    void finished( Kross::Action *action );
++
++  private:
++    Kross::Action *action;
+ };
+ #endif
diff --git a/resources/maildir/autotests/maildir/.root.directory/child1/new/.keep b/resources/maildir/autotests/maildir/.root.directory/child1/new/.keep
new file mode 100644 (file)
index 0000000..2f1d07a
--- /dev/null
@@ -0,0 +1 @@
+force git to include this file
diff --git a/resources/maildir/autotests/maildir/.root.directory/child1/tmp/.keep b/resources/maildir/autotests/maildir/.root.directory/child1/tmp/.keep
new file mode 100644 (file)
index 0000000..2f1d07a
--- /dev/null
@@ -0,0 +1 @@
+force git to include this file
diff --git a/resources/maildir/autotests/maildir/.root.directory/child2/.keep b/resources/maildir/autotests/maildir/.root.directory/child2/.keep
new file mode 100644 (file)
index 0000000..2f1d07a
--- /dev/null
@@ -0,0 +1 @@
+force git to include this file
diff --git a/resources/maildir/autotests/maildir/.root.directory/child2/cur/.keep b/resources/maildir/autotests/maildir/.root.directory/child2/cur/.keep
new file mode 100644 (file)
index 0000000..2f1d07a
--- /dev/null
@@ -0,0 +1 @@
+force git to include this file
diff --git a/resources/maildir/autotests/maildir/.root.directory/child2/new/.keep b/resources/maildir/autotests/maildir/.root.directory/child2/new/.keep
new file mode 100644 (file)
index 0000000..2f1d07a
--- /dev/null
@@ -0,0 +1 @@
+force git to include this file
diff --git a/resources/maildir/autotests/maildir/.root.directory/child2/tmp/.keep b/resources/maildir/autotests/maildir/.root.directory/child2/tmp/.keep
new file mode 100644 (file)
index 0000000..2f1d07a
--- /dev/null
@@ -0,0 +1 @@
+force git to include this file
diff --git a/resources/maildir/autotests/maildir/root/cur/1237726845.6570.BejQg!2,S b/resources/maildir/autotests/maildir/root/cur/1237726845.6570.BejQg!2,S
new file mode 100644 (file)
index 0000000..c9b1d83
--- /dev/null
@@ -0,0 +1,171 @@
+Return-Path: <commitfilter@new.kstuff.org>
+Received: from localhost (localhost [127.0.0.1])
+        by smykowski.kdab.net (Cyrus v2.2.12) with LMTPA;
+        Sun, 22 Mar 2009 13:45:00 +0100
+X-Sieve: CMU Sieve 2.2
+Received: from localhost (localhost [127.0.0.1])
+       by smykowski.kdab.net (Postfix) with ESMTP id 8C4FFE6C79B
+       for <asok@kdab.net>; Sun, 22 Mar 2009 13:45:00 +0100 (CET)
+Received: from smykowski.kdab.net ([127.0.0.1])
+ by localhost (smykowski.kdab.net [127.0.0.1]) (amavisd-new, port 10024)
+ with ESMTP id 09703-05 for <asok@kdab.net>;
+ Sun, 22 Mar 2009 13:45:00 +0100 (CET)
+Received: from localhost (localhost [127.0.0.1])
+       by smykowski.kdab.net (Postfix) with ESMTP id 163D2E6C7AF
+       for <asok@kdab.net>; Sun, 22 Mar 2009 13:45:00 +0100 (CET)
+Received: from kdeget.osuosl.org (kdeget.osuosl.org [140.211.166.77])
+       by smykowski.kdab.net (Postfix) with ESMTP id 31945E6C79E
+       for <asok@kdab.net>; Sun, 22 Mar 2009 13:44:59 +0100 (CET)
+Received: from ktown.kde.org ([131.246.120.250])
+       by kdeget.osuosl.org with smtp (Exim 4.63)
+       (envelope-from <kde-commits-bounces-+commitfilter=new.kstuff.org@kde.org>)
+       id 1LlN8R-0005Jr-VE
+       for commitfilter@new.kstuff.org; Sun, 22 Mar 2009 13:50:40 +0100
+Received: (qmail 7667 invoked by uid 72); 22 Mar 2009 12:50:33 -0000
+Received: (qmail 7658 invoked from network); 22 Mar 2009 12:50:31 -0000
+Received: from unknown (HELO office.kde.org) (195.135.221.67)
+       by ktown.kde.org with SMTP; 22 Mar 2009 12:50:29 -0000
+Received: from svn.kde.org (localhost [127.0.0.1])
+       by office.kde.org (Postfix) with SMTP id 93E9F18E
+       for <kde-commits@kde.org>; Sun, 22 Mar 2009 13:50:30 +0100 (CET)
+Received: (nullmailer pid 25707 invoked by uid 30);
+       Sun, 22 Mar 2009 12:50:30 -0000
+From: Volker Krause <vkrause@kde.org>
+To: kde-commits@kde.org
+Subject: playground/pim/akonaditest/resourcetester
+X-Commit-Directories: (0) trunk/playground/pim/akonaditest/resourcetester
+MIME-Version: 1.0
+Content-Type: text/plain;
+  charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Date: Sun, 22 Mar 2009 12:50:30 +0000
+Message-Id: <1237726230.394911.25706.nullmailer@svn.kde.org>
+X-BeenThere: kde-commits@kde.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+Reply-To: kde-commits@kde.org
+List-Id: Notification of KDE commits <kde-commits.kde.org>
+List-Unsubscribe: <https://mail.kde.org/mailman/listinfo/kde-commits>,
+       <mailto:kde-commits-request@kde.org?subject=unsubscribe>
+List-Post: <mailto:kde-commits@kde.org>
+List-Help: <mailto:kde-commits-request@kde.org?subject=help>
+List-Subscribe: <https://mail.kde.org/mailman/listinfo/kde-commits>,
+       <mailto:kde-commits-request@kde.org?subject=subscribe>
+X-Virus-Scanned: by amavisd-new at kdab.net
+X-Kolab-Scheduling-Message: FALSE
+X-UID: 26669
+X-Length: 5694
+Status: RO
+X-Status: RC
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+SVN commit 942677 by vkrause:
+
+Add a safety timeout in case we do not receive the synchronized() signal
+or the resource hangs during syncing. The first seems to happen randomly
+if syncing is extremely fast.
+
+
+ M  +40 -0     resourcesynchronizationjob.cpp  
+ M  +1 -1      resourcesynchronizationjob.h  
+
+
+--- trunk/playground/pim/akonaditest/resourcetester/resourcesynchronizationjob.cpp #942676:942677
+@@ -18,12 +18,14 @@
+ #include "resourcesynchronizationjob.h"
+ #include <akonadi/agentinstance.h>
++#include <akonadi/agentmanager.h>
+ #include <KDebug>
+ #include <KLocale>
+ #include <QDBusConnection>
+ #include <QDBusInterface>
++#include <QTimer>
+ namespace Akonadi
+ {
+@@ -31,15 +33,31 @@
+ class ResourceSynchronizationJobPrivate
+ {
+   public:
++    ResourceSynchronizationJobPrivate() :
++      interface( 0 ),
++      safetyTimer( 0 ),
++      timeoutCount( 0 )
++    {}
++
+     AgentInstance instance;
+     QDBusInterface* interface;
++    QTimer* safetyTimer;
++    int timeoutCount;
++    static int timeoutCountLimit;
+ };
++int ResourceSynchronizationJobPrivate::timeoutCountLimit = 60;
++
+ ResourceSynchronizationJob::ResourceSynchronizationJob(const AgentInstance& instance, QObject* parent) :
+   KJob( parent ),
+   d( new ResourceSynchronizationJobPrivate )
+ {
+   d->instance = instance;
++  d->safetyTimer = new QTimer( this );
++  connect( d->safetyTimer, SIGNAL(timeout()), SLOT(slotTimeout()) );
++  d->safetyTimer->setInterval( 10 * 1000 );
++  d->safetyTimer->setSingleShot( false );
++  d->safetyTimer->start();
+ }
+ ResourceSynchronizationJob::~ResourceSynchronizationJob()
+@@ -72,9 +90,31 @@
+ void ResourceSynchronizationJob::slotSynchronized()
+ {
++  disconnect( d->interface, SIGNAL(synchronized()), this, SLOT(slotSynchronized()) );
++  d->safetyTimer->stop();
+   emitResult();
+ }
++void ResourceSynchronizationJob::slotTimeout()
++{
++  d->instance = AgentManager::self()->instance( d->instance.identifier() );
++  d->timeoutCount++;
++
++  if ( d->timeoutCount > d->timeoutCountLimit ) {
++    d->safetyTimer->stop();
++    setError( UserDefinedError );
++    setErrorText( i18n( "Resource synchronization timed out." ) );
++    emitResult();
++    return;
++  }
++
++  if ( d->instance.status() == AgentInstance::Idle ) {
++    // try again, we might have lost the synchronized() signal
++    kDebug() << "trying again to sync resource" << d->instance.identifier();
++    d->instance.synchronize();
++  }
+ }
++}
++
+ #include "resourcesynchronizationjob.moc"
+--- trunk/playground/pim/akonaditest/resourcetester/resourcesynchronizationjob.h #942676:942677
+@@ -27,7 +27,6 @@
+ /**
+   Synchronizes a given resource.
+-  @todo Add safety timeouts.
+ */
+ class ResourceSynchronizationJob : public KJob
+ {
+@@ -48,6 +47,7 @@
+   private slots:
+     void slotSynchronized();
++    void slotTimeout();
+   private:
+     ResourceSynchronizationJobPrivate* const d;
diff --git a/resources/maildir/autotests/maildir/root/new/.keep b/resources/maildir/autotests/maildir/root/new/.keep
new file mode 100644 (file)
index 0000000..2f1d07a
--- /dev/null
@@ -0,0 +1 @@
+force git to include this file
diff --git a/resources/maildir/autotests/maildir/root/tmp/.keep b/resources/maildir/autotests/maildir/root/tmp/.keep
new file mode 100644 (file)
index 0000000..2f1d07a
--- /dev/null
@@ -0,0 +1 @@
+force git to include this file
diff --git a/resources/maildir/autotests/synctest.cpp b/resources/maildir/autotests/synctest.cpp
new file mode 100644 (file)
index 0000000..d8b19c6
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+    Copyright 2009 Constantin Berzan <exit3219@gmail.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "synctest.h"
+
+#include <QDBusInterface>
+#include <QTime>
+
+#include <QDebug>
+
+#include <AkonadiCore/AgentInstance>
+#include <AkonadiCore/AgentManager>
+#include <AkonadiCore/Control>
+#include <qtest_akonadi.h>
+#include <QSignalSpy>
+
+#define TIMES 100 // How many times to sync.
+#define TIMEOUT 10 // How many seconds to wait before declaring the resource dead.
+
+using namespace Akonadi;
+
+void SyncTest::initTestCase()
+{
+    QVERIFY(Control::start());
+    QTest::qWait(1000);
+}
+
+void SyncTest::testSync()
+{
+    AgentInstance instance = AgentManager::self()->instance(QStringLiteral("akonadi_maildir_resource_0"));
+    QVERIFY(instance.isValid());
+
+    for (int i = 0; i < 100; i++) {
+        QDBusInterface *interface = new QDBusInterface(
+            QStringLiteral("org.freedesktop.Akonadi.Resource.%1").arg(instance.identifier()),
+            QStringLiteral("/"), QStringLiteral("org.freedesktop.Akonadi.Resource"), QDBusConnection::sessionBus(), this);
+        QVERIFY(interface->isValid());
+        QTime t;
+        t.start();
+        instance.synchronize();
+        QSignalSpy spy(interface, SIGNAL(synchronized()));
+        QVERIFY(spy.wait(TIMEOUT * 1000));
+        qDebug() << "Sync attempt" << i << "in" << t.elapsed() << "ms.";
+    }
+}
+
+QTEST_AKONADIMAIN(SyncTest)
+
diff --git a/resources/maildir/autotests/synctest.h b/resources/maildir/autotests/synctest.h
new file mode 100644 (file)
index 0000000..30dc892
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+    Copyright 2009 Constantin Berzan <exit3219@gmail.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef SYNCTEST_H
+#define SYNCTEST_H
+
+#include <QtCore/QObject>
+
+#include <AkonadiCore/Collection>
+
+/**
+  This syncs the resource again and again, watching out for "lost"
+  synchronized() signals.
+ */
+class SyncTest : public QObject
+{
+    Q_OBJECT
+
+private Q_SLOTS:
+    void initTestCase();
+    void testSync();
+
+private:
+
+};
+
+#endif
diff --git a/resources/maildir/autotests/testmail.mbox b/resources/maildir/autotests/testmail.mbox
new file mode 100644 (file)
index 0000000..f14d60d
--- /dev/null
@@ -0,0 +1,19 @@
+From: Volker Krause <vkrause@kde.org>
+To: kde-commits@kde.org
+Subject: playground/pim/akonaditest/resourcetester
+MIME-Version: 1.0
+Content-Type: text/plain;
+  charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Date: Sun, 22 Mar 2009 12:50:30 +0000
+Message-Id: <1237726230.394911.25706.nullmailer@svn.kde.org>
+
+SVN commit 942677 by vkrause:
+
+Add a safety timeout in case we do not receive the synchronized() signal
+or the resource hangs during syncing. The first seems to happen randomly
+if syncing is extremely fast.
+
+
+ M  +40 -0     resourcesynchronizationjob.cpp  
+ M  +1 -1      resourcesynchronizationjob.h  
diff --git a/resources/maildir/autotests/unittestenv/config.xml b/resources/maildir/autotests/unittestenv/config.xml
new file mode 100644 (file)
index 0000000..0995c4b
--- /dev/null
@@ -0,0 +1,6 @@
+<config>
+  <kdehome>kdehome</kdehome>
+  <confighome>xdgconfig</confighome>
+  <datahome>xdglocal</datahome>
+  <agent synchronize="true">akonadi_maildir_resource</agent>
+</config>
diff --git a/resources/maildir/autotests/unittestenv/kdehome/share/config/akonadi-firstrunrc b/resources/maildir/autotests/unittestenv/kdehome/share/config/akonadi-firstrunrc
new file mode 100644 (file)
index 0000000..1cac492
--- /dev/null
@@ -0,0 +1,3 @@
+[ProcessedDefaults]
+defaultaddressbook=done
+defaultcalendar=done
diff --git a/resources/maildir/autotests/unittestenv/kdehome/share/config/akonadi_maildir_resource_0rc b/resources/maildir/autotests/unittestenv/kdehome/share/config/akonadi_maildir_resource_0rc
new file mode 100644 (file)
index 0000000..f1a0fd1
--- /dev/null
@@ -0,0 +1,2 @@
+[General]
+Path[$e]=$HOME/.local/share/mail
diff --git a/resources/maildir/autotests/unittestenv/kdehome/share/config/kdebugrc b/resources/maildir/autotests/unittestenv/kdehome/share/config/kdebugrc
new file mode 100644 (file)
index 0000000..32317f7
--- /dev/null
@@ -0,0 +1,110 @@
+[0]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[5250]
+InfoOutput=2
+
+[5251]
+InfoOutput=2
+
+[5252]
+InfoOutput=2
+
+[5253]
+InfoOutput=2
+
+[5254]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[5255]
+InfoOutput=2
+
+[5256]
+InfoOutput=2
+
+[5257]
+InfoOutput=2
+
+[5258]
+InfoOutput=2
+
+[5259]
+InfoOutput=2
+
+[5260]
+InfoOutput=2
+
+[5261]
+InfoOutput=2
+
+[5262]
+InfoOutput=2
+
+[5263]
+InfoOutput=2
+
+[5264]
+InfoOutput=2
+
+[5265]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[5266]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[5295]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[5324]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[7129]
+InfoOutput=2
diff --git a/resources/maildir/autotests/unittestenv/kdehome/share/config/kdedrc b/resources/maildir/autotests/unittestenv/kdehome/share/config/kdedrc
new file mode 100644 (file)
index 0000000..41d1781
--- /dev/null
@@ -0,0 +1,3 @@
+[General]
+CheckSycoca=false
+CheckFileStamps=false
diff --git a/resources/maildir/autotests/unittestenv/kdehome/share/config/kwalletrc b/resources/maildir/autotests/unittestenv/kdehome/share/config/kwalletrc
new file mode 100644 (file)
index 0000000..8ba29ca
--- /dev/null
@@ -0,0 +1,2 @@
+[Wallet]
+Enabled=false
diff --git a/resources/maildir/autotests/unittestenv/kdehome/share/config/qttestrc b/resources/maildir/autotests/unittestenv/kdehome/share/config/qttestrc
new file mode 100644 (file)
index 0000000..2e2f28e
--- /dev/null
@@ -0,0 +1,2 @@
+[Notification Messages]
+WalletMigrate=false
diff --git a/resources/maildir/autotests/unittestenv/kdehome/testdata.xml b/resources/maildir/autotests/unittestenv/kdehome/testdata.xml
new file mode 100644 (file)
index 0000000..8cf871b
--- /dev/null
@@ -0,0 +1,4 @@
+<knut>
+ <collection rid="123" name="sink" content="inode/directory,message/rfc822">
+ </collection>
+</knut>
diff --git a/resources/maildir/autotests/unittestenv/xdgconfig/akonadi/akonadiserverrc b/resources/maildir/autotests/unittestenv/xdgconfig/akonadi/akonadiserverrc
new file mode 100644 (file)
index 0000000..7f738ce
--- /dev/null
@@ -0,0 +1,4 @@
+[%General]
+
+[Search]
+Manager=Dummy
diff --git a/resources/maildir/autotests/unittestenv/xdglocal/.keep b/resources/maildir/autotests/unittestenv/xdglocal/.keep
new file mode 100644 (file)
index 0000000..2f1d07a
--- /dev/null
@@ -0,0 +1 @@
+force git to include this file
diff --git a/resources/maildir/configdialog.cpp b/resources/maildir/configdialog.cpp
new file mode 100644 (file)
index 0000000..f74726f
--- /dev/null
@@ -0,0 +1,146 @@
+/*
+    Copyright (c) 2008 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "configdialog.h"
+#include "settings.h"
+#include "resources/folderarchivesettings/folderarchivesettingpage.h"
+
+#include <maildir.h>
+
+#include <kconfigdialogmanager.h>
+#include <kurlrequester.h>
+#include <klineedit.h>
+#include <QVBoxLayout>
+#include <QDialogButtonBox>
+
+using KPIM::Maildir;
+using namespace Akonadi_Maildir_Resource;
+
+ConfigDialog::ConfigDialog(MaildirSettings *settings, const QString &identifier, QWidget *parent) :
+    QDialog(parent),
+    mSettings(settings),
+    mToplevelIsContainer(false)
+{
+    setWindowTitle(i18n("Select a MailDir folder"));
+    QWidget *mainWidget = new QWidget(this);
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    setLayout(mainLayout);
+    mainLayout->addWidget(mainWidget);
+    ui.setupUi(mainWidget);
+    mFolderArchiveSettingPage = new FolderArchiveSettingPage(identifier);
+    mFolderArchiveSettingPage->loadSettings();
+    ui.tabWidget->addTab(mFolderArchiveSettingPage, i18n("Archive Folder"));
+
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    mOkButton = buttonBox->button(QDialogButtonBox::Ok);
+    mOkButton->setDefault(true);
+    mOkButton->setShortcut(Qt::CTRL | Qt::Key_Return);
+    connect(buttonBox, &QDialogButtonBox::accepted, this, &ConfigDialog::accept);
+    connect(buttonBox, &QDialogButtonBox::rejected, this, &ConfigDialog::reject);
+    mainLayout->addWidget(buttonBox);
+
+    mManager = new KConfigDialogManager(this, mSettings);
+    mManager->updateWidgets();
+    ui.kcfg_Path->setMode(KFile::Directory | KFile::ExistingOnly);
+    ui.kcfg_Path->setUrl(QUrl::fromLocalFile(mSettings->path()));
+
+    connect(mOkButton, &QPushButton::clicked, this, &ConfigDialog::save);
+    connect(ui.kcfg_Path->lineEdit(), &QLineEdit::textChanged, this, &ConfigDialog::checkPath);
+    ui.kcfg_Path->lineEdit()->setFocus();
+    checkPath();
+    readConfig();
+}
+
+ConfigDialog::~ConfigDialog()
+{
+    writeConfig();
+}
+
+void ConfigDialog::readConfig()
+{
+    KConfigGroup group(KSharedConfig::openConfig(), "ConfigDialog");
+    const QSize size = group.readEntry("Size", QSize(600, 400));
+    if (size.isValid()) {
+        resize(size);
+    }
+}
+
+void ConfigDialog::writeConfig()
+{
+    KConfigGroup group(KSharedConfig::openConfig(), "ConfigDialog");
+    group.writeEntry("Size", size());
+    group.sync();
+}
+
+void ConfigDialog::checkPath()
+{
+    if (ui.kcfg_Path->url().isEmpty()) {
+        ui.statusLabel->setText(i18n("The selected path is empty."));
+        mOkButton->setEnabled(false);
+        return;
+    }
+    bool ok = false;
+    mToplevelIsContainer = false;
+    QDir d(ui.kcfg_Path->url().toLocalFile());
+
+    if (d.exists()) {
+        Maildir md(d.path());
+        if (!md.isValid(false)) {
+            Maildir md2(d.path(), true);
+            if (md2.isValid(false)) {
+                ui.statusLabel->setText(i18n("The selected path contains valid Maildir folders."));
+                mToplevelIsContainer = true;
+                ok = true;
+            } else {
+                ui.statusLabel->setText(md.lastError());
+            }
+        } else {
+            ui.statusLabel->setText(i18n("The selected path is a valid Maildir."));
+            ok = true;
+        }
+    } else {
+        d.cdUp();
+        if (d.exists()) {
+            ui.statusLabel->setText(i18n("The selected path does not exist yet, a new Maildir will be created."));
+            mToplevelIsContainer = true;
+            ok = true;
+        } else {
+            ui.statusLabel->setText(i18n("The selected path does not exist."));
+        }
+    }
+    mOkButton->setEnabled(ok);
+}
+
+void ConfigDialog::save()
+{
+    mFolderArchiveSettingPage->writeSettings();
+    mManager->updateSettings();
+    QString path = ui.kcfg_Path->url().isLocalFile() ? ui.kcfg_Path->url().toLocalFile() : ui.kcfg_Path->url().path();
+    mSettings->setPath(path);
+    mSettings->setTopLevelIsContainer(mToplevelIsContainer);
+    mSettings->save();
+
+    if (ui.kcfg_Path->url().isLocalFile()) {
+        QDir d(path);
+        if (!d.exists()) {
+            d.mkpath(ui.kcfg_Path->url().toLocalFile());
+        }
+    }
+}
+
diff --git a/resources/maildir/configdialog.h b/resources/maildir/configdialog.h
new file mode 100644 (file)
index 0000000..e675ac0
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+    Copyright (c) 2008 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef CONFIGDIALOG_H
+#define CONFIGDIALOG_H
+
+#include <QDialog>
+
+#include "ui_settings.h"
+class QPushButton;
+class KConfigDialogManager;
+namespace Akonadi_Maildir_Resource
+{
+class MaildirSettings;
+}
+class FolderArchiveSettingPage;
+class ConfigDialog : public QDialog
+{
+    Q_OBJECT
+public:
+    explicit ConfigDialog(Akonadi_Maildir_Resource::MaildirSettings *settings, const QString &identifier, QWidget *parent = Q_NULLPTR);
+    ~ConfigDialog();
+
+private Q_SLOTS:
+    void checkPath();
+    void save();
+
+private:
+    void readConfig();
+    void writeConfig();
+    Ui::ConfigDialog ui;
+    KConfigDialogManager *mManager;
+    FolderArchiveSettingPage *mFolderArchiveSettingPage;
+    Akonadi_Maildir_Resource::MaildirSettings *mSettings;
+    bool mToplevelIsContainer;
+    QPushButton *mOkButton;
+};
+
+#endif
diff --git a/resources/maildir/libmaildir/CMakeLists.txt b/resources/maildir/libmaildir/CMakeLists.txt
new file mode 100644 (file)
index 0000000..572afa5
--- /dev/null
@@ -0,0 +1,24 @@
+
+if (BUILD_TESTING)
+   add_subdirectory( autotests )
+endif()
+
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_maildir_resource\")
+
+set(maildir_LIB_SRCS keycache.cpp maildir.cpp)
+ecm_qt_declare_logging_category(maildir_LIB_SRCS HEADER libmaildir_debug.h IDENTIFIER LIBMAILDIR_LOG CATEGORY_NAME log_libmaildir)
+
+add_library(maildir  ${maildir_LIB_SRCS})
+generate_export_header(maildir BASE_NAME maildir)
+
+target_link_libraries(maildir
+  PUBLIC
+    KF5::AkonadiMime
+  PRIVATE
+    KF5::I18n
+    Qt5::Network
+)
+
+set_target_properties(maildir PROPERTIES VERSION ${KDEPIMRUNTIME_LIB_VERSION} SOVERSION ${KDEPIMRUNTIME_LIB_SOVERSION} )
+
+install(TARGETS maildir ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP)
diff --git a/resources/maildir/libmaildir/autotests/CMakeLists.txt b/resources/maildir/libmaildir/autotests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..90e1ec9
--- /dev/null
@@ -0,0 +1,19 @@
+include(ECMAddTests)
+
+find_package(Qt5Test CONFIG REQUIRED)
+
+set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} )
+
+include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/..   )
+
+########### next target ###############
+
+set(testmaildir_SRCS testmaildir.cpp )
+
+
+add_executable( testmaildir ${testmaildir_SRCS} )
+add_test( testmaildir testmaildir )
+ecm_mark_as_test(maildir-testmaildir)
+
+target_link_libraries(testmaildir  Qt5::Test  KF5::AkonadiMime maildir)
+
diff --git a/resources/maildir/libmaildir/autotests/testmaildir.cpp b/resources/maildir/libmaildir/autotests/testmaildir.cpp
new file mode 100644 (file)
index 0000000..1131573
--- /dev/null
@@ -0,0 +1,436 @@
+/*
+  This file is part of the kpimutils library.
+
+  Copyright (C) 2007 Till Adam <adam@kde.org>
+
+  This library is free software; you can redistribute it and/or
+  modify it under the terms of the GNU Library General Public
+  License version 2 as published by the Free Software Foundation.
+
+  This library is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  Library General Public License for more details.
+
+  You should have received a copy of the GNU Library General Public License
+  along with this library; see the file COPYING.LIB.  If not, write to
+  the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+  Boston, MA 02110-1301, USA.
+*/
+
+#include "testmaildir.h"
+
+#include <memory>
+
+#include <QDir>
+#include <QFile>
+
+#include <qtest.h>
+
+#include <QTemporaryDir>
+#include <akonadi/kmime/messageflags.h>
+
+QTEST_MAIN(MaildirTest)
+
+#include "../maildir.h"
+using namespace KPIM;
+
+static const char *testDir = "libmaildir-unit-test";
+static const char *testString = "From: theDukeOfMonmouth@uk.gov\n \nTo: theDukeOfBuccleuch@uk.gov\n\ntest\n";
+static const char *testStringHeaders = "From: theDukeOfMonmouth@uk.gov\n \nTo: theDukeOfBuccleuch@uk.gov\n";
+
+void MaildirTest::init()
+{
+    QString tmpPath(QDir::tempPath() + QLatin1Char('/') +  QLatin1String(testDir));
+    QDir().mkpath(tmpPath);
+    m_temp = new QTemporaryDir(tmpPath);
+
+    QDir temp(m_temp->path());
+    QVERIFY(temp.exists());
+
+    temp.mkdir(QStringLiteral("new"));
+    QVERIFY(temp.exists(QLatin1String("new")));
+    temp.mkdir(QStringLiteral("cur"));
+    QVERIFY(temp.exists(QLatin1String("cur")));
+    temp.mkdir(QStringLiteral("tmp"));
+    QVERIFY(temp.exists(QLatin1String("tmp")));
+}
+
+void MaildirTest::cleanup()
+{
+    m_temp->remove();
+    QDir d(m_temp->path());
+    const QString subFolderPath(QStringLiteral(".%1.directory").arg(d.dirName()));
+    QDir((subFolderPath)).removeRecursively();
+
+    delete m_temp;
+    m_temp = 0;
+}
+
+void MaildirTest::fillDirectory(const QString &name, int limit)
+{
+    QFile file;
+    QDir::setCurrent(m_temp->path() + QLatin1Char('/') + name);
+    for (int i = 0; i < limit; i++) {
+        file.setFileName(QLatin1String("testmail-") + QString::number(i));
+        file.open(QIODevice::WriteOnly);
+        file.write(testString);
+        file.flush();
+        file.close();
+    }
+}
+
+void MaildirTest::createSubFolders()
+{
+    QDir d(m_temp->path());
+    const QString subFolderPath(QStringLiteral(".%1.directory").arg(d.dirName()));
+    d.cdUp();
+    d.mkdir(subFolderPath);
+    d.cd(subFolderPath);
+    d.mkdir(QStringLiteral("foo"));
+    d.mkdir(QStringLiteral("barbar"));
+    d.mkdir(QStringLiteral("bazbaz"));
+}
+
+void MaildirTest::fillNewDirectory()
+{
+    fillDirectory(QStringLiteral("new"), 140);
+}
+
+void MaildirTest::fillCurrentDirectory()
+{
+    fillDirectory(QStringLiteral("cur"), 20);
+}
+
+void MaildirTest::testMaildirInstantiation()
+{
+    Maildir d(QStringLiteral("/foo/bar/Mail"));
+    Maildir d2(d);
+    Maildir d3;
+    d3 = d;
+    QVERIFY(d == d2);
+    QVERIFY(d3 == d2);
+    QVERIFY(d == d3);
+    QCOMPARE(d.path(), QString(QLatin1String("/foo/bar/Mail")));
+    QCOMPARE(d.name(), QString(QLatin1String("Mail")));
+
+    QVERIFY(!d.isValid(false));
+
+    Maildir good(m_temp->path());
+    QVERIFY(good.isValid(false));
+
+    QDir temp(m_temp->path());
+    temp.rmdir(QStringLiteral("new"));
+    QVERIFY(!good.isValid(false));
+    QVERIFY(!good.lastError().isEmpty());
+
+    Maildir root1(QStringLiteral("/foo/bar/Mail"), true);
+    QVERIFY(root1.isRoot());
+
+    Maildir root1Copy = root1;
+    QCOMPARE(root1Copy.path(), root1.path());
+    QCOMPARE(root1Copy.isRoot(), root1.isRoot());
+
+    // FIXME test insufficient permissions?
+}
+
+void MaildirTest::testMaildirListing()
+{
+    fillNewDirectory();
+
+    Maildir d(m_temp->path());
+    QStringList entries = d.entryList();
+
+    QCOMPARE(entries.count(), 140);
+
+    fillCurrentDirectory();
+    entries = d.entryList();
+    QCOMPARE(entries.count(), 160);
+}
+
+void MaildirTest::testMaildirAccess()
+{
+    fillCurrentDirectory();
+    Maildir d(m_temp->path());
+    QStringList entries = d.entryList();
+    QCOMPARE(entries.count(), 20);
+
+    QByteArray data = d.readEntry(entries[0]);
+    QVERIFY(!data.isEmpty());
+    QCOMPARE(data, QByteArray(testString));
+}
+
+void MaildirTest::testMaildirReadHeaders()
+{
+    fillCurrentDirectory();
+    Maildir d(m_temp->path());
+    QStringList entries = d.entryList();
+    QCOMPARE(entries.count(), 20);
+
+    QByteArray data = d.readEntryHeaders(entries[0]);
+    QVERIFY(!data.isEmpty());
+    QCOMPARE(data, QByteArray(testStringHeaders));
+}
+
+void MaildirTest::testMaildirWrite()
+{
+    fillCurrentDirectory();
+    Maildir d(m_temp->path());
+    QStringList entries = d.entryList();
+    QCOMPARE(entries.count(), 20);
+
+    QByteArray data = d.readEntry(entries[0]);
+    QByteArray data2 = "changed\n";
+    QVERIFY(d.writeEntry(entries[0], data2));
+    QCOMPARE(data2, d.readEntry(entries[0]));
+}
+
+void MaildirTest::testMaildirAppend()
+{
+    Maildir d(m_temp->path());
+    QByteArray data = "newentry\n";
+    QString key = d.addEntry(data);
+    QVERIFY(!key.isEmpty());
+    QCOMPARE(data, d.readEntry(key));
+}
+
+void MaildirTest::testMaildirCreation()
+{
+    QString p(QStringLiteral("CREATETEST"));
+    QString tmpPath(QDir::tempPath() + QLatin1Char('/') +  p);
+    QDir().mkpath(tmpPath);
+    std::auto_ptr<QTemporaryDir> temp(new QTemporaryDir(tmpPath));
+    Maildir d(temp->path() + p);
+    QVERIFY(!d.isValid(false));
+    d.create();
+    QVERIFY(d.isValid(false));
+}
+
+void MaildirTest::testMaildirRemoveEntry()
+{
+    Maildir d(m_temp->path());
+    QByteArray data = "newentry\n";
+    QString key = d.addEntry(data);
+    QVERIFY(!key.isEmpty());
+    QCOMPARE(data, d.readEntry(key));
+    QVERIFY(d.removeEntry(key));
+    QVERIFY(d.readEntry(key).isEmpty());
+}
+
+void MaildirTest::testMaildirListSubfolders()
+{
+    fillNewDirectory();
+
+    Maildir d(m_temp->path());
+    QStringList entries = d.subFolderList();
+
+    QVERIFY(entries.isEmpty());
+
+    createSubFolders();
+
+    entries = d.subFolderList();
+    QVERIFY(!entries.isEmpty());
+    QCOMPARE(entries.count(), 3);
+}
+
+void MaildirTest::testMaildirCreateSubfolder()
+{
+    Maildir d(m_temp->path());
+    QStringList entries = d.subFolderList();
+    QVERIFY(entries.isEmpty());
+
+    d.addSubFolder(QStringLiteral("subFolderTest"));
+    entries = d.subFolderList();
+    QVERIFY(!entries.isEmpty());
+    QCOMPARE(entries.count(), 1);
+    Maildir child = d.subFolder(entries.first());
+    QVERIFY(child.isValid(false));
+}
+
+void MaildirTest::testMaildirRemoveSubfolder()
+{
+    Maildir d(m_temp->path());
+    QVERIFY(d.isValid(false));
+
+    QString folderPath = d.addSubFolder(QStringLiteral("subFolderTest"));
+    QVERIFY(!folderPath.isEmpty());
+    QVERIFY(folderPath.endsWith(QLatin1String(".directory/subFolderTest")));
+    bool removingWorked = d.removeSubFolder(QStringLiteral("subFolderTest"));
+    QVERIFY(removingWorked);
+}
+
+void MaildirTest::testMaildirRename()
+{
+    Maildir d(m_temp->path());
+    QVERIFY(d.isValid(false));
+
+    QString folderPath = d.addSubFolder(QStringLiteral("rename me!"));
+    QVERIFY(!folderPath.isEmpty());
+
+    Maildir d2(folderPath);
+    QVERIFY(d2.isValid(false));
+    QVERIFY(d2.rename(QLatin1String("renamed")));
+    QCOMPARE(d2.name(), QString(QLatin1String("renamed")));
+
+    // same again, should not fail
+    QVERIFY(d2.rename(QLatin1String("renamed")));
+    QCOMPARE(d2.name(), QString(QLatin1String("renamed")));
+
+    // already existing name
+    QVERIFY(!d.addSubFolder(QLatin1String("this name is already taken")).isEmpty());
+    QVERIFY(!d2.rename(QLatin1String("this name is already taken")));
+}
+
+void MaildirTest::testMaildirMoveTo()
+{
+    Maildir d(m_temp->path());
+    QVERIFY(d.isValid(false));
+
+    QString folderPath1 = d.addSubFolder(QStringLiteral("child1"));
+    QVERIFY(!folderPath1.isEmpty());
+
+    Maildir d2(folderPath1);
+    QVERIFY(d2.isValid(false));
+
+    QDir d2Dir(d2.path());
+    QVERIFY(d2Dir.exists());
+
+    QString folderPath11 = d2.addSubFolder(QStringLiteral("grandchild1"));
+
+    Maildir d21(folderPath11);
+    QVERIFY(d21.isValid(false));
+
+    QDir d2SubDir(Maildir::subDirPathForFolderPath(d2.path()));
+    QVERIFY(d2SubDir.exists());
+
+    QString folderPath2 = d.addSubFolder(QStringLiteral("child2"));
+    QVERIFY(!folderPath2.isEmpty());
+
+    Maildir d3(folderPath2);
+    QVERIFY(d3.isValid(false));
+
+    // move child1 to child2
+    QVERIFY(d2.moveTo(d3));
+
+    Maildir d31 = d3.subFolder(QStringLiteral("child1"));
+    QVERIFY(d31.isValid(false));
+
+    QVERIFY(!d2Dir.exists());
+    QVERIFY(!d2SubDir.exists());
+
+    QDir d31Dir(d31.path());
+    QVERIFY(d31Dir.exists());
+
+    QDir d31SubDir(Maildir::subDirPathForFolderPath(d31.path()));
+    QVERIFY(d31SubDir.exists());
+
+    Maildir d311 = d31.subFolder(QStringLiteral("grandchild1"));
+    QVERIFY(d311.isValid(false));
+
+    // try moving again
+    d2 = Maildir(folderPath1);
+    QVERIFY(!d2.isValid(false));
+    QVERIFY(!d2.moveTo(d3));
+}
+
+void MaildirTest::testMaildirFlagsReading()
+{
+    QFile file;
+    const QStringList markers = QStringList() << QStringLiteral("P") << QStringLiteral("R") << QStringLiteral("S") << QStringLiteral("F") << QStringLiteral("FPRS");
+    QDir::setCurrent(m_temp->path() + QLatin1Char('/') + QStringLiteral("cur"));
+    for (int i = 0; i < 6; i++) {
+        QString fileName = QLatin1String("testmail-") + QString::number(i);
+        if (i < 5) {
+            fileName +=
+#ifdef Q_OS_WIN
+                QLatin1String("!2,")
+#else
+                QLatin1String(":2,")
+#endif
+                + markers[i];
+        }
+        file.setFileName(fileName);
+        file.open(QIODevice::WriteOnly);
+        file.write(testString);
+        file.flush();
+        file.close();
+    }
+
+    Maildir d(m_temp->path());
+    QStringList entries = d.entryList();
+    // Maildir::entryList() doesn't sort for performance reasons,
+    // do it here to make test sequence reliable.
+    entries.sort();
+
+    QCOMPARE(entries.count(), 6);
+
+    Akonadi::Item::Flags flags = d.readEntryFlags(entries[0]);
+    QCOMPARE(flags.count(), 1);
+    QVERIFY(flags.contains(Akonadi::MessageFlags::Forwarded));
+
+    flags = d.readEntryFlags(entries[1]);
+    QCOMPARE(flags.count(), 1);
+    QVERIFY(flags.contains(Akonadi::MessageFlags::Replied));
+
+    flags = d.readEntryFlags(entries[2]);
+    QCOMPARE(flags.count(), 1);
+    QVERIFY(flags.contains(Akonadi::MessageFlags::Seen));
+
+    flags = d.readEntryFlags(entries[3]);
+    QCOMPARE(flags.count(), 1);
+    QVERIFY(flags.contains(Akonadi::MessageFlags::Flagged));
+
+    flags = d.readEntryFlags(entries[4]);
+    QCOMPARE(flags.count(), 4);
+    QVERIFY(flags.contains(Akonadi::MessageFlags::Forwarded));
+    QVERIFY(flags.contains(Akonadi::MessageFlags::Replied));
+    QVERIFY(flags.contains(Akonadi::MessageFlags::Seen));
+    QVERIFY(flags.contains(Akonadi::MessageFlags::Flagged));
+
+    flags = d.readEntryFlags(entries[5]);
+    QVERIFY(flags.isEmpty());
+}
+
+void MaildirTest::testMaildirFlagsWriting_data()
+{
+    QTest::addColumn<QString>("origDir");
+    QTest::addColumn<QString>("origFileName");
+    QTest::newRow("cur/") << "cur" << "testmail";
+    QTest::newRow("cur/S") << "cur" << "testmail:2,S";   // wrongly marked as "seen" on disk (#289428)
+    QTest::newRow("new/") << "new" << "testmail";
+    QTest::newRow("new/S") << "new" << "testmail:2,S";
+}
+
+void MaildirTest::testMaildirFlagsWriting()
+{
+    QFETCH(QString, origDir);
+    QFETCH(QString, origFileName);
+
+    // create an initialy new mail
+    QFile file;
+    QDir::setCurrent(m_temp->path());
+    file.setFileName(origDir + QLatin1Char('/') + origFileName);
+    file.open(QIODevice::WriteOnly);
+    file.write(testString);
+    file.flush();
+    file.close();
+
+    // add a single flag
+    Maildir d(m_temp->path());
+    const QStringList entries = d.entryList();
+    QCOMPARE(entries.size(), 1);
+    QVERIFY(QFile::exists(origDir + QLatin1Char('/') + entries[0]));
+    const QString newKey = d.changeEntryFlags(entries[0], Akonadi::Item::Flags() << Akonadi::MessageFlags::Seen);
+    // make sure the new key exists
+    QCOMPARE(newKey, d.entryList()[0]);
+    QVERIFY(QFile::exists(QStringLiteral("cur/") + newKey));
+    // and it's the right file
+    QCOMPARE(d.readEntry(newKey), QByteArray(testString));
+    // now check the file name
+    QVERIFY(newKey.endsWith(QLatin1String("2,S")));
+    // and more flags
+    const QString newKey2 = d.changeEntryFlags(newKey, Akonadi::Item::Flags() << Akonadi::MessageFlags::Seen << Akonadi::MessageFlags::Replied);
+    // check the file name, and the sorting of markers
+    QVERIFY(newKey2.endsWith(QLatin1String("2,RS")));
+    QVERIFY(QFile::exists(QStringLiteral("cur/") + newKey2));
+}
diff --git a/resources/maildir/libmaildir/autotests/testmaildir.h b/resources/maildir/libmaildir/autotests/testmaildir.h
new file mode 100644 (file)
index 0000000..0198fe6
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+  This file is part of the kpimutils library.
+
+  Copyright (c) 2007 Till Adam <adam@kde.org>
+
+  This library is free software; you can redistribute it and/or
+  modify it under the terms of the GNU Library General Public
+  License as published by the Free Software Foundation; either
+  version 2 of the License, or (at your option) any later version.
+
+  This library is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  Library General Public License for more details.
+
+  You should have received a copy of the GNU Library General Public License
+  along with this library; see the file COPYING.LIB.  If not, write to
+  the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+  Boston, MA 02110-1301, USA.
+*/
+
+#ifndef MAILDIRTEST_H
+#define MAILDIRTEST_H
+
+#include <QObject>
+
+class QTemporaryDir;
+
+class MaildirTest : public QObject
+{
+    Q_OBJECT
+private Q_SLOTS:
+    void init();
+    void testMaildirInstantiation();
+    void testMaildirCreation();
+    void testMaildirListing();
+    void testMaildirAccess();
+    void testMaildirReadHeaders();
+    void testMaildirWrite();
+    void testMaildirAppend();
+    void testMaildirRemoveEntry();
+    void testMaildirListSubfolders();
+    void testMaildirCreateSubfolder();
+    void testMaildirRemoveSubfolder();
+    void testMaildirRename();
+    void testMaildirMoveTo();
+    void testMaildirFlagsReading();
+    void testMaildirFlagsWriting_data();
+    void testMaildirFlagsWriting();
+    void cleanup();
+private:
+    void fillDirectory(const QString &name, int limit);
+    void fillNewDirectory();
+    void fillCurrentDirectory();
+    void createSubFolders();
+    QTemporaryDir *m_temp;
+};
+
+#endif
diff --git a/resources/maildir/libmaildir/keycache.cpp b/resources/maildir/libmaildir/keycache.cpp
new file mode 100644 (file)
index 0000000..9e72ed3
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+    Copyright (C) 2012  Andras Mantia <amantia@kde.org>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#include "keycache.h"
+#include <QDirIterator>
+
+KeyCache *KeyCache::mSelf = Q_NULLPTR;
+
+void KeyCache::addKeys(const QString &dir)
+{
+    if (!mNewKeys.contains(dir)) {
+        mNewKeys.insert(dir, listNew(dir));
+        //qCDebug(LIBMAILDIR_LOG) << "Added new keys for: " << dir;
+    }
+
+    if (!mCurKeys.contains(dir)) {
+        mCurKeys.insert(dir, listCurrent(dir));
+        //qCDebug(LIBMAILDIR_LOG) << "Added cur keys for: " << dir;
+    }
+}
+
+void KeyCache::refreshKeys(const QString &dir)
+{
+    mNewKeys.remove(dir);
+    mCurKeys.remove(dir);
+    addKeys(dir);
+}
+
+void KeyCache::addNewKey(const QString &dir, const QString &key)
+{
+    mNewKeys[dir].insert(key);
+    // qCDebug(LIBMAILDIR_LOG) << "Added new key for : " << dir << " key: " << key;
+}
+
+void KeyCache::addCurKey(const QString &dir, const QString &key)
+{
+    mCurKeys[dir].insert(key);
+    // qCDebug(LIBMAILDIR_LOG) << "Added cur key for : " << dir << " key:" << key;
+}
+
+void KeyCache::removeKey(const QString &dir, const QString &key)
+{
+    //qCDebug(LIBMAILDIR_LOG) << "Removed new and cur key for: " << dir << " key:" << key;
+    mNewKeys[dir].remove(key);
+    mCurKeys[dir].remove(key);
+}
+
+bool KeyCache::isCurKey(const QString &dir, const QString &key) const
+{
+    return mCurKeys.value(dir).contains(key);
+}
+
+bool KeyCache::isNewKey(const QString &dir, const QString &key) const
+{
+    return mNewKeys.value(dir).contains(key);
+}
+
+QSet<QString> KeyCache::listNew(const QString &dir) const
+{
+    QSet<QString> keys;
+    QDirIterator d(dir + QLatin1String("/new"), QDir::Files);
+    while (d.hasNext()) {
+        d.next();
+        keys.insert(d.fileName());
+    }
+    return keys;
+}
+
+QSet<QString> KeyCache::listCurrent(const QString &dir) const
+{
+    QSet<QString> keys;
+    QDirIterator d(dir + QLatin1String("/cur"), QDir::Files);
+    while (d.hasNext()) {
+        d.next();
+        keys.insert(d.fileName());
+    }
+    return keys;
+}
+
diff --git a/resources/maildir/libmaildir/keycache.h b/resources/maildir/libmaildir/keycache.h
new file mode 100644 (file)
index 0000000..bda603e
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+    Copyright (C) 2012  Andras Mantia <amantia@kde.org>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#ifndef KEYCACHE_H
+#define KEYCACHE_H
+
+/** @brief a cache for the maildir keys (file names in cur/new folders).
+ *  It is used to find if a file is in cur or new
+ */
+
+#include <QSet>
+#include <QHash>
+
+class KeyCache
+{
+
+public:
+    static KeyCache *self()
+    {
+        if (!mSelf) {
+            mSelf = new KeyCache();
+        }
+        return mSelf;
+    }
+
+    /** Find the new and cur keys on the disk for @param dir and add them to the cache */
+    void addKeys(const QString &dir);
+
+    /** Refresh the new and cur keys for @param dir */
+    void refreshKeys(const QString &dir);
+
+    /** Add a "new" key for @param dir. */
+    void addNewKey(const QString &dir, const QString &key);
+
+    /** Add a "cur" key for @param dir. */
+    void addCurKey(const QString &dir, const QString &key);
+
+    /** Remove all keys associated with @param dir. */
+    void removeKey(const QString &dir, const QString &key);
+
+    /** Check if the @param key is a "cur" key in @param dir */
+    bool isCurKey(const QString &dir, const QString &key) const;
+
+    /** Check if the @param key is a "new" key in @param dir */
+    bool isNewKey(const QString &dir, const QString &key) const;
+
+private:
+    KeyCache()
+    {
+    }
+
+    QSet<QString> listNew(const QString &dir) const;
+
+    QSet<QString> listCurrent(const QString &dir) const;
+
+    QHash< QString, QSet<QString> > mNewKeys;
+    QHash< QString, QSet<QString> > mCurKeys;
+
+    static KeyCache *mSelf;
+
+};
+
+#endif // KEYCACHE_H
diff --git a/resources/maildir/libmaildir/maildir.cpp b/resources/maildir/libmaildir/maildir.cpp
new file mode 100644 (file)
index 0000000..4812ea7
--- /dev/null
@@ -0,0 +1,869 @@
+/*
+    Copyright (c) 2007 Till Adam <adam@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "maildir.h"
+#include "keycache.h"
+
+#include <QDateTime>
+#include <QDir>
+#include <QFileInfo>
+#include <QHostInfo>
+#include <QUuid>
+
+#include "libmaildir_debug.h"
+#include <KLocalizedString>
+#include <Akonadi/KMime/MessageFlags>
+
+#include <time.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+
+//Define it to get more debug output to expense of operating speed
+// #define DEBUG_KEYCACHE_CONSITENCY
+
+static void initRandomSeed()
+{
+    static bool init = false;
+    if (!init) {
+        unsigned int seed;
+        init = true;
+        int fd = ::open("/dev/urandom", O_RDONLY);
+        if (fd < 0 || ::read(fd, &seed, sizeof(seed)) != sizeof(seed)) {
+            // No /dev/urandom... try something else.
+            srand(getpid());
+            seed = rand() + time(Q_NULLPTR);
+        }
+
+        if (fd >= 0) {
+            close(fd);
+        }
+
+        qsrand(seed);
+    }
+}
+
+using namespace KPIM;
+
+static QRegExp statusSeparatorRx()
+{
+    static const QRegExp expr(QStringLiteral(":|!"));
+    return expr;
+}
+
+class Maildir::Private
+{
+public:
+    Private(const QString &p, bool isRoot)
+        : path(p), isRoot(isRoot)
+    {
+        hostName = QHostInfo::localHostName();
+        // The default implementation of QUuid::createUuid() doesn't use
+        // a seed that is random enough. Therefor we use our own initialization
+        // until this issue will be fixed in Qt 4.7.
+        initRandomSeed();
+
+        //Cache object is created the first time this runs.
+        //It will live throughout the lifetime of the application
+        KeyCache::self()->addKeys(path);
+    }
+
+    Private(const Private &rhs)
+    {
+        path = rhs.path;
+        isRoot = rhs.isRoot;
+        hostName = rhs.hostName;
+    }
+
+    bool operator==(const Private &rhs) const
+    {
+        return path == rhs.path;
+    }
+    bool accessIsPossible(bool createMissingFolders = true);
+    bool canAccess(const QString &path) const;
+
+    QStringList subPaths() const
+    {
+        QStringList paths;
+        paths << path + QLatin1String("/cur");
+        paths << path + QLatin1String("/new");
+        paths << path + QLatin1String("/tmp");
+        return paths;
+    }
+
+    QStringList listNew() const
+    {
+        QDir d(path + QLatin1String("/new"));
+        d.setSorting(QDir::NoSort);
+        return d.entryList(QDir::Files);
+    }
+
+    QStringList listCurrent() const
+    {
+        QDir d(path + QLatin1String("/cur"));
+        d.setSorting(QDir::NoSort);
+        return d.entryList(QDir::Files);
+    }
+
+    QString findRealKey(const QString &key) const
+    {
+        KeyCache *keyCache = KeyCache::self();
+        if (keyCache->isNewKey(path, key)) {
+#ifdef DEBUG_KEYCACHE_CONSITENCY
+            if (!QFile::exists(path + QString::fromLatin1("/new/") + key)) {
+                qCDebug(LIBMAILDIR_LOG) << "WARNING: key is in cache, but the file is gone: " << path + QString::fromLatin1("/new/") + key;
+            }
+#endif
+            return path + QLatin1String("/new/") + key;
+        }
+        if (keyCache->isCurKey(path, key)) {
+#ifdef DEBUG_KEYCACHE_CONSITENCY
+            if (!QFile::exists(path + QString::fromLatin1("/cur/") + key)) {
+                qCDebug(LIBMAILDIR_LOG) << "WARNING: key is in cache, but the file is gone: " << path + QString::fromLatin1("/cur/") + key;
+            }
+#endif
+            return path + QLatin1String("/cur/") + key;
+        }
+        QString realKey = path + QLatin1String("/new/") + key;
+
+        QFile f(realKey);
+        if (f.exists()) {
+            keyCache->addNewKey(path, key);
+        } else  { //not in "new", search in "cur"
+            realKey = path + QLatin1String("/cur/") + key;
+            QFile f2(realKey);
+            if (f2.exists()) {
+                keyCache->addCurKey(path, key);
+            } else {
+                realKey.clear(); //not in "cur" either
+            }
+        }
+
+        return realKey;
+    }
+
+    static QString subDirNameForFolderName(const QString &folderName)
+    {
+        return QStringLiteral(".%1.directory").arg(folderName);
+    }
+
+    QString subDirPath() const
+    {
+        QDir dir(path);
+        return subDirNameForFolderName(dir.dirName());
+    }
+
+    bool moveAndRename(QDir &dest, const QString &newName)
+    {
+        if (!dest.exists()) {
+            qCDebug(LIBMAILDIR_LOG) << "Destination does not exist";
+            return false;
+        }
+        if (dest.exists(newName) || dest.exists(subDirNameForFolderName(newName))) {
+            qCDebug(LIBMAILDIR_LOG) << "New name already in use";
+            return false;
+        }
+
+        if (!dest.rename(path, newName)) {
+            qCDebug(LIBMAILDIR_LOG) << "Failed to rename maildir";
+            return false;
+        }
+        const QDir subDirs(Maildir::subDirPathForFolderPath(path));
+        if (subDirs.exists() && !dest.rename(subDirs.path(), subDirNameForFolderName(newName))) {
+            qCDebug(LIBMAILDIR_LOG) << "Failed to rename subfolders";
+            return false;
+        }
+
+        path = dest.path() + QDir::separator() + newName;
+        return true;
+    }
+
+    QString path;
+    bool isRoot;
+    QString hostName;
+    QString lastError;
+};
+
+Maildir::Maildir(const QString &path, bool isRoot)
+    : d(new Private(path, isRoot))
+{
+}
+
+void Maildir::swap(const Maildir &rhs)
+{
+    Private *p = d;
+    d = new Private(*rhs.d);
+    delete p;
+}
+
+Maildir::Maildir(const Maildir &rhs)
+    : d(new Private(*rhs.d))
+
+{
+}
+
+Maildir &Maildir::operator= (const Maildir &rhs)
+{
+    // copy and swap, exception safe, and handles assignment to self
+    Maildir temp(rhs);
+    swap(temp);
+    return *this;
+}
+
+bool Maildir::operator== (const Maildir &rhs) const
+{
+    return *d == *rhs.d;
+}
+
+Maildir::~Maildir()
+{
+    delete d;
+}
+
+bool Maildir::Private::canAccess(const QString &path) const
+{
+    //return access( QFile::encodeName( path ), R_OK | W_OK | X_OK ) != 0;
+    // FIXME X_OK?
+    QFileInfo d(path);
+    return d.isReadable() && d.isWritable();
+}
+
+bool Maildir::Private::accessIsPossible(bool createMissingFolders)
+{
+    QStringList paths = subPaths();
+
+    paths.prepend(path);
+
+    Q_FOREACH (const QString &p, paths) {
+        if (!QFile::exists(p)) {
+            if (!createMissingFolders) {
+                lastError = i18n("Error opening %1; this folder is missing.", p);
+                return false;
+            }
+            QDir().mkpath(p);
+            if (!QFile::exists(p)) {
+                lastError = i18n("Error opening %1; this folder is missing.", p);
+                return false;
+            }
+        }
+        if (!canAccess(p)) {
+            lastError = i18n("Error opening %1; either this is not a valid "
+                             "maildir folder, or you do not have sufficient access permissions.", p);
+            return false;
+        }
+    }
+    return true;
+}
+
+bool Maildir::isValid(bool createMissingFolders) const
+{
+    if (path().isEmpty()) {
+        return false;
+    }
+    if (!d->isRoot) {
+        if (d->accessIsPossible(createMissingFolders)) {
+            return true;
+        }
+    } else {
+        Q_FOREACH (const QString &sf, subFolderList()) {
+            const Maildir subMd = Maildir(path() + QLatin1Char('/') + sf);
+            if (!subMd.isValid()) {
+                d->lastError = subMd.lastError();
+                return false;
+            }
+        }
+        return true;
+    }
+    return false;
+}
+
+bool Maildir::isRoot() const
+{
+    return d->isRoot;
+}
+
+bool Maildir::create()
+{
+    // FIXME: in a failure case, this will leave partially created dirs around
+    // we should clean them up, but only if they didn't previously existed...
+    Q_FOREACH (const QString &p, d->subPaths()) {
+        QDir dir(p);
+        if (!dir.exists(p)) {
+            if (!dir.mkpath(p)) {
+                return false;
+            }
+        }
+    }
+    return true;
+}
+
+QString Maildir::path() const
+{
+    return d->path;
+}
+
+QString Maildir::name() const
+{
+    const QDir dir(d->path);
+    return dir.dirName();
+}
+
+QString Maildir::addSubFolder(const QString &path)
+{
+    if (!isValid()) {
+        return QString();
+    }
+
+    // make the subdir dir
+    QDir dir(d->path);
+    if (!d->isRoot) {
+        dir.cdUp();
+        if (!dir.exists(d->subDirPath())) {
+            dir.mkdir(d->subDirPath());
+        }
+        dir.cd(d->subDirPath());
+    }
+
+    const QString fullPath = dir.path() + QLatin1Char('/') + path;
+    Maildir subdir(fullPath);
+    if (subdir.create()) {
+        return fullPath;
+    }
+    return QString();
+}
+
+bool Maildir::removeSubFolder(const QString &folderName)
+{
+    if (!isValid()) {
+        return false;
+    }
+    QDir dir(d->path);
+    if (!d->isRoot) {
+        dir.cdUp();
+        if (!dir.exists(d->subDirPath())) {
+            return false;
+        }
+        dir.cd(d->subDirPath());
+    }
+    if (!dir.exists(folderName)) {
+        return false;
+    }
+
+    // remove it recursively
+    bool result = QDir(dir.absolutePath() + QLatin1Char('/') + folderName).removeRecursively();
+    QString subfolderName = subDirNameForFolderName(folderName);
+    if (dir.exists(subfolderName)) {
+        result &= QDir(dir.absolutePath() + QLatin1Char('/') + subfolderName).removeRecursively();
+    }
+    return result;
+}
+
+Maildir Maildir::subFolder(const QString &subFolder) const
+{
+    // make the subdir dir
+    QDir dir(d->path);
+    if (!d->isRoot) {
+        dir.cdUp();
+        if (dir.exists(d->subDirPath())) {
+            dir.cd(d->subDirPath());
+        }
+    }
+    return Maildir(dir.path() + QLatin1Char('/') + subFolder);
+}
+
+Maildir Maildir::parent() const
+{
+    if (!isValid() || d->isRoot) {
+        return Maildir();
+    }
+    QDir dir(d->path);
+    dir.cdUp();
+    if (!dir.dirName().startsWith(QLatin1Char('.')) || !dir.dirName().endsWith(QLatin1String(".directory"))) {
+        return Maildir();
+    }
+    const QString parentName = dir.dirName().mid(1, dir.dirName().size() - 11);
+    dir.cdUp();
+    dir.cd(parentName);
+    return Maildir(dir.path());
+}
+
+QStringList Maildir::entryList() const
+{
+    QStringList result;
+    if (isValid()) {
+        result += d->listNew();
+        result += d->listCurrent();
+    }
+    //  qCDebug(LIBMAILDIR_LOG) <<"Maildir::entryList()" << result;
+    return result;
+}
+
+QStringList Maildir::listCurrent() const
+{
+    QStringList result;
+    if (isValid()) {
+        result += d->listCurrent();
+    }
+    return result;
+}
+
+QString Maildir::findRealKey(const QString &key) const
+{
+    return d->findRealKey(key);
+}
+
+QStringList Maildir::listNew() const
+{
+    QStringList result;
+    if (isValid()) {
+        result += d->listNew();
+    }
+    return result;
+}
+
+QString Maildir::pathToNew() const
+{
+    if (isValid()) {
+        return d->path + QLatin1String("/new");
+    }
+    return QString();
+}
+
+QString Maildir::pathToCurrent() const
+{
+    if (isValid()) {
+        return d->path + QLatin1String("/cur");
+    }
+    return QString();
+}
+
+QString Maildir::subDirPath() const
+{
+    QDir dir(d->path);
+    dir.cdUp();
+    return dir.path() + QDir::separator() + d->subDirPath();
+}
+
+QStringList Maildir::subFolderList() const
+{
+    QDir dir(d->path);
+
+    // the root maildir has its subfolders directly beneath it
+    if (!d->isRoot) {
+        dir.cdUp();
+        if (!dir.exists(d->subDirPath())) {
+            return QStringList();
+        }
+        dir.cd(d->subDirPath());
+    }
+    dir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
+    QStringList entries = dir.entryList();
+    entries.removeAll(QStringLiteral("cur"));
+    entries.removeAll(QStringLiteral("new"));
+    entries.removeAll(QStringLiteral("tmp"));
+    return entries;
+}
+
+QByteArray Maildir::readEntry(const QString &key) const
+{
+    QByteArray result;
+
+    QString realKey(d->findRealKey(key));
+    if (realKey.isEmpty()) {
+        // FIXME error handling?
+        qCWarning(LIBMAILDIR_LOG) << "Maildir::readEntry unable to find: " << key;
+        d->lastError = i18n("Cannot locate mail file %1.", key);
+        return result;
+    }
+
+    QFile f(realKey);
+    if (!f.open(QIODevice::ReadOnly)) {
+        d->lastError = i18n("Cannot open mail file %1.", realKey);
+        return result;
+    }
+
+    // FIXME be safer than this
+    result = f.readAll();
+
+    return result;
+}
+qint64 Maildir::size(const QString &key) const
+{
+    QString realKey(d->findRealKey(key));
+    if (realKey.isEmpty()) {
+        // FIXME error handling?
+        qCWarning(LIBMAILDIR_LOG) << "Maildir::size unable to find: " << key;
+        d->lastError = i18n("Cannot locate mail file %1.", key);
+        return -1;
+    }
+
+    QFileInfo info(realKey);
+    if (!info.exists()) {
+        d->lastError = i18n("Cannot open mail file %1.", realKey);
+        return -1;
+    }
+
+    return info.size();
+}
+
+QDateTime Maildir::lastModified(const QString &key) const
+{
+    const QString realKey(d->findRealKey(key));
+    if (realKey.isEmpty()) {
+        qCWarning(LIBMAILDIR_LOG) << "Maildir::lastModified unable to find: " << key;
+        d->lastError = i18n("Cannot locate mail file %1.", key);
+        return QDateTime();
+    }
+
+    const QFileInfo info(realKey);
+    if (!info.exists()) {
+        return QDateTime();
+    }
+
+    return info.lastModified();
+}
+
+QByteArray Maildir::readEntryHeadersFromFile(const QString &file) const
+{
+    QByteArray result;
+
+    QFile f(file);
+    if (!f.open(QIODevice::ReadOnly)) {
+        // FIXME error handling?
+        qCWarning(LIBMAILDIR_LOG) << "Maildir::readEntryHeaders unable to find: " << file;
+        d->lastError = i18n("Cannot locate mail file %1.", file);
+        return result;
+    }
+    f.map(0, qMin((qint64)8000, f.size()));
+    forever {
+        QByteArray line = f.readLine();
+        if (line.isEmpty() || line.startsWith('\n')) {
+            break;
+        }
+        result.append(line);
+    }
+    return result;
+}
+
+QByteArray Maildir::readEntryHeaders(const QString &key) const
+{
+    const QString realKey(d->findRealKey(key));
+    if (realKey.isEmpty()) {
+        qCWarning(LIBMAILDIR_LOG) << "Maildir::readEntryHeaders unable to find: " << key;
+        d->lastError = i18n("Cannot locate mail file %1.", key);
+        return QByteArray();
+    }
+
+    return readEntryHeadersFromFile(realKey);
+}
+
+static QString createUniqueFileName()
+{
+    qint64 time = QDateTime::currentMSecsSinceEpoch() / 1000;
+    int r = qrand() % 1000;
+    QString identifier = QLatin1String("R") + QString::number(r);
+
+    QString fileName = QString::number(time) + QLatin1Char('.') + identifier + QLatin1Char('.');
+
+    return fileName;
+}
+
+bool Maildir::writeEntry(const QString &key, const QByteArray &data)
+{
+    QString realKey(d->findRealKey(key));
+    if (realKey.isEmpty()) {
+        // FIXME error handling?
+        qCWarning(LIBMAILDIR_LOG) << "Maildir::writeEntry unable to find: " << key;
+        d->lastError = i18n("Cannot locate mail file %1.", key);
+        return false;
+    }
+    QFile f(realKey);
+    bool result = f.open(QIODevice::WriteOnly);
+    result = result & (f.write(data) != -1);
+    f.close();
+    if (!result) {
+        d->lastError = i18n("Cannot write to mail file %1.", realKey);
+        return false;
+    }
+    return true;
+}
+
+QString Maildir::addEntry(const QByteArray &data)
+{
+    QString uniqueKey;
+    QString key;
+    QString finalKey;
+    QString curKey;
+
+    // QUuid doesn't return globally unique identifiers, therefor we query until we
+    // get one that doesn't exists yet
+    do {
+        uniqueKey = createUniqueFileName() + d->hostName;
+        key = d->path + QLatin1String("/tmp/") + uniqueKey;
+        finalKey = d->path + QLatin1String("/new/") + uniqueKey;
+        curKey = d->path + QLatin1String("/cur/") + uniqueKey;
+    } while (QFile::exists(key) || QFile::exists(finalKey) || QFile::exists(curKey));
+
+    QFile f(key);
+    bool result = f.open(QIODevice::WriteOnly);
+    result = result & (f.write(data) != -1);
+    f.close();
+    if (!result) {
+        d->lastError = i18n("Cannot write to mail file %1.", key);
+        return QString();
+    }
+    /*
+     * FIXME:
+     *
+     * The whole point of the locking free maildir idea is that the moves between
+     * the internal directories are atomic. Afaik QFile::rename does not guarantee
+     * that, so this will need to be done properly. - ta
+     *
+     * For reference: http://trolltech.com/developer/task-tracker/index_html?method=entry&id=211215
+     */
+    if (!f.rename(finalKey)) {
+        qCWarning(LIBMAILDIR_LOG) << "Maildir: Failed to add entry: " << finalKey  << "! Error: " << f.errorString();
+        d->lastError = i18n("Failed to create mail file %1. The error was: %2", finalKey, f.errorString());
+        return QString();
+    }
+    KeyCache *keyCache = KeyCache::self();
+    keyCache->removeKey(d->path, key);   //remove all keys, be it "cur" or "new" first
+    keyCache->addNewKey(d->path, key);   //and add a key for "new", as the mail was moved there
+    return uniqueKey;
+}
+
+bool Maildir::removeEntry(const QString &key)
+{
+    QString realKey(d->findRealKey(key));
+    if (realKey.isEmpty()) {
+        qCWarning(LIBMAILDIR_LOG) << "Maildir::removeEntry unable to find: " << key;
+        return false;
+    }
+    KeyCache *keyCache = KeyCache::self();
+    keyCache->removeKey(d->path, key);
+    return QFile::remove(realKey);
+}
+
+QString Maildir::changeEntryFlags(const QString &key, const Akonadi::Item::Flags &flags)
+{
+    QString realKey(d->findRealKey(key));
+    if (realKey.isEmpty()) {
+        qCWarning(LIBMAILDIR_LOG) << "Maildir::changeEntryFlags unable to find: " << key;
+        d->lastError = i18n("Cannot locate mail file %1.", key);
+        return QString();
+    }
+
+    const QRegExp rx = statusSeparatorRx();
+    QString finalKey = key.left(key.indexOf(rx));
+
+    QStringList mailDirFlags;
+    Q_FOREACH (const Akonadi::Item::Flag &flag, flags) {
+        if (flag == Akonadi::MessageFlags::Forwarded) {
+            mailDirFlags << QStringLiteral("P");
+        }
+        if (flag == Akonadi::MessageFlags::Replied) {
+            mailDirFlags << QStringLiteral("R");
+        }
+        if (flag == Akonadi::MessageFlags::Seen) {
+            mailDirFlags << QStringLiteral("S");
+        }
+        if (flag == Akonadi::MessageFlags::Deleted) {
+            mailDirFlags << QStringLiteral("T");
+        }
+        if (flag == Akonadi::MessageFlags::Flagged) {
+            mailDirFlags << QStringLiteral("F");
+        }
+    }
+    mailDirFlags.sort();
+    if (!mailDirFlags.isEmpty()) {
+#ifdef Q_OS_WIN
+        finalKey.append(QLatin1String("!2,") + mailDirFlags.join(QString()));
+#else
+        finalKey.append(QLatin1String(":2,") + mailDirFlags.join(QString()));
+#endif
+    }
+
+    QString newUniqueKey = finalKey; //key without path
+    finalKey.prepend(d->path + QLatin1String("/cur/"));
+
+    if (realKey == finalKey) {
+        // Somehow it already is named this way (e.g. migration bug -> wrong status in akonadi)
+        return newUniqueKey;
+    }
+
+    QFile f(realKey);
+    if (QFile::exists(finalKey)) {
+        QFile destFile(finalKey);
+        QByteArray destContent;
+        if (destFile.open(QIODevice::ReadOnly)) {
+            destContent = destFile.readAll();
+            destFile.close();
+        }
+        QByteArray sourceContent;
+        if (f.open(QIODevice::ReadOnly)) {
+            sourceContent = f.readAll();
+            f.close();
+        }
+
+        if (destContent != sourceContent) {
+            QString newFinalKey = QLatin1String("1-") + newUniqueKey;
+            int i = 1;
+            while (QFile::exists(d->path + QLatin1String("/cur/") + newFinalKey)) {
+                i++;
+                newFinalKey = QString::number(i) + QLatin1Char('-') + newUniqueKey;
+            }
+            finalKey = d->path + QLatin1String("/cur/") + newFinalKey;
+        } else {
+            QFile::remove(finalKey);   //they are the same
+        }
+    }
+
+    if (!f.rename(finalKey)) {
+        qCWarning(LIBMAILDIR_LOG) << "Maildir: Failed to rename entry: " << f.fileName() << " to "  << finalKey  << "! Error: " << f.errorString();
+        d->lastError = i18n("Failed to update the file name %1 to %2 on the disk. The error was: %3.", f.fileName(), finalKey, f.errorString());
+        return QString();
+    }
+
+    KeyCache *keyCache = KeyCache::self();
+    keyCache->removeKey(d->path, key);
+    keyCache->addCurKey(d->path, newUniqueKey);
+
+    return newUniqueKey;
+}
+
+Akonadi::Item::Flags Maildir::readEntryFlags(const QString &key) const
+{
+    Akonadi::Item::Flags flags;
+
+    const QRegExp rx = statusSeparatorRx();
+    const int index = key.indexOf(rx);
+    if (index != -1) {
+        const QString mailDirFlags = key.mid(index + 3);   // after "(:|!)2,"
+        const int flagSize(mailDirFlags.size());
+        for (int i = 0; i < flagSize; ++i) {
+            if (mailDirFlags[i] == QLatin1Char('P')) {
+                flags << Akonadi::MessageFlags::Forwarded;
+            } else if (mailDirFlags[i] == QLatin1Char('R')) {
+                flags << Akonadi::MessageFlags::Replied;
+            } else if (mailDirFlags[i] == QLatin1Char('S')) {
+                flags << Akonadi::MessageFlags::Seen;
+            } else if (mailDirFlags[i] == QLatin1Char('F')) {
+                flags << Akonadi::MessageFlags::Flagged;
+            }
+        }
+    }
+
+    return flags;
+}
+
+bool Maildir::moveTo(const Maildir &newParent)
+{
+    if (d->isRoot) {
+        return false;    // not supported
+    }
+
+    QDir newDir(newParent.path());
+    if (!newParent.d->isRoot) {
+        newDir.cdUp();
+        if (!newDir.exists(newParent.d->subDirPath())) {
+            newDir.mkdir(newParent.d->subDirPath());
+        }
+        newDir.cd(newParent.d->subDirPath());
+    }
+
+    QDir currentDir(d->path);
+    currentDir.cdUp();
+
+    if (newDir == currentDir) {
+        return true;
+    }
+
+    return d->moveAndRename(newDir, name());
+}
+
+bool Maildir::rename(const QString &newName)
+{
+    if (name() == newName) {
+        return true;
+    }
+    if (d->isRoot) {
+        return false;    // not (yet) supported
+    }
+
+    QDir dir(d->path);
+    dir.cdUp();
+
+    return d->moveAndRename(dir, newName);
+}
+
+QString Maildir::moveEntryTo(const QString &key, const Maildir &destination)
+{
+    const QString realKey(d->findRealKey(key));
+    if (realKey.isEmpty()) {
+        qCWarning(LIBMAILDIR_LOG) << "Unable to find: " << key;
+        d->lastError = i18n("Cannot locate mail file %1.", key);
+        return QString();
+    }
+    QFile f(realKey);
+    // ### is this safe regarding the maildir locking scheme?
+    const QString targetKey = destination.path() + QDir::separator() + QLatin1String("new") + QDir::separator() + key;
+    if (!f.rename(targetKey)) {
+        qCDebug(LIBMAILDIR_LOG) << "Failed to rename" << realKey << "to" << targetKey << "! Error: " << f.errorString();;
+        d->lastError = f.errorString();
+        return QString();
+    }
+
+    KeyCache *keyCache = KeyCache::self();
+
+    keyCache->addNewKey(destination.path(), key);
+    keyCache->removeKey(d->path, key);
+
+    return key;
+}
+
+QString Maildir::subDirPathForFolderPath(const QString &folderPath)
+{
+    QDir dir(folderPath);
+    const QString dirName = dir.dirName();
+    dir.cdUp();
+    return QFileInfo(dir, Private::subDirNameForFolderName(dirName)).filePath();
+}
+
+QString Maildir::subDirNameForFolderName(const QString &folderName)
+{
+    return Private::subDirNameForFolderName(folderName);
+}
+
+void Maildir::removeCachedKeys(const QStringList &keys)
+{
+    KeyCache *keyCache = KeyCache::self();
+    Q_FOREACH (const QString &key, keys) {
+        keyCache->removeKey(d->path, key);
+    }
+}
+
+void Maildir::refreshKeyCache()
+{
+    KeyCache::self()->refreshKeys(d->path);
+}
+
+QString Maildir::lastError() const
+{
+    return d->lastError;
+}
diff --git a/resources/maildir/libmaildir/maildir.h b/resources/maildir/libmaildir/maildir.h
new file mode 100644 (file)
index 0000000..aedcd8d
--- /dev/null
@@ -0,0 +1,258 @@
+/*
+    Copyright (c) 2007 Till Adam <adam@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef MAILDIR_H
+#define MAILDIR_H
+
+#include "maildir_export.h"
+
+#include <QString>
+#include <QStringList>
+#include <item.h>
+
+class QDateTime;
+
+namespace KPIM
+{
+
+class MAILDIR_EXPORT Maildir
+{
+public:
+    /**
+      Create a new Maildir object.
+      @param path The path to the maildir, if @p isRoot is @c false, that's the path
+      to the folder containing the cur/new/tmp folders, if @p isRoot is @c true this
+      is the path to a folder containing a number of maildirs.
+      @param isRoot Indicate whether this is a maildir containing mails and various
+      sub-folders or a container only containing maildirs.
+    */
+    explicit Maildir(const QString &path = QString(), bool isRoot = false);
+    /* Copy constructor */
+    Maildir(const Maildir &rhs);
+    /* Copy operator */
+    Maildir &operator=(const Maildir &rhs);
+    /** Equality comparison */
+    bool operator==(const Maildir &rhs) const;
+    /* Destructor */
+    ~Maildir();
+
+    /** Returns whether the maildir has all the necessary subdirectories,
+     * that they are readable, etc.
+     * @param createMissingFolders if true (the default), the cur/new/tmp folders are created if they are missing
+     */
+    bool isValid(bool createMissingFolders = true) const;
+
+    /**
+     * Returns whether this is a normal maildir or a container containing maildirs.
+     */
+    bool isRoot() const;
+
+    /**
+     * Make a valid maildir at the path of this Maildir object. This involves
+     * creating the necessary subdirs, etc. Note that an empty Maildir is
+     * not valid, unless it is given  valid path, or until create( ) is
+     * called on it.
+     */
+    bool create();
+
+    /**
+     * Returns the path of this maildir.
+     */
+    QString path() const;
+
+    /**
+     * Returns the name of this maildir.
+     */
+    QString name() const;
+
+    /**
+     * Returns the list of items (mails) in the maildir. These are keys, which
+     * map to filenames, internally, but that's an implementation detail, which
+     * should not be relied on.
+     */
+    QStringList entryList() const;
+
+    /** Returns the list of items (mails) in the maildirs "new" folder. These are keys, which
+     * map to filenames, internally, but that's an implementation detail, which
+     * should not be relied on.
+     */
+    QStringList listNew() const;
+
+    /** Returns the list of items (mails) in the maildirs "cur" folder. These are keys, which
+     * map to filenames, internally, but that's an implementation detail, which
+     * should not be relied on.
+     */
+    QStringList listCurrent() const;
+
+    /** Return the path to the "new" directory */
+    QString pathToNew() const;
+
+    /** Return the path to the "cur" directory */
+    QString pathToCurrent() const;
+
+    /**
+     * Returns the full path to the subdir (the NAME.directory folder ).
+     **/
+    QString subDirPath() const;
+
+    /**
+     * Return the full path to the file identified by key (it can be either in the "new" or "cur" folder
+     **/
+    QString findRealKey(const QString &key) const;
+
+    /**
+     * Returns the list of subfolders, as names (relative paths). Use the
+     * subFolder method to get Maildir objects representing them.
+     */
+    QStringList subFolderList() const;
+
+    /**
+     * Adds subfolder with the given @p folderName.
+     * @return an empty string on failure or the full path of the new subfolder
+     *         on success
+     */
+    QString addSubFolder(const QString &folderName);
+
+    /**
+     * Removes subfolder with the given @p folderName. Returns success or failure.
+     */
+    bool removeSubFolder(const QString &folderName);
+
+    /**
+     * Returns a Maildir object for the given @p folderName. If such a folder
+     * exists, the Maildir object will be valid, otherwise you can call create()
+     * on it, to make a subfolder with that name.
+     */
+    Maildir subFolder(const QString &folderName) const;
+
+    /**
+     * Returns the parent Maildir object for this Maildir, if there is one (ie. this is not the root).
+     */
+    Maildir parent() const;
+
+    /**
+     * Returns the size of the file in the maildir with the given @p key or \c -1 if key is not valid.
+     * @since 4.2
+     */
+    qint64 size(const QString &key) const;
+
+    /**
+     * Returns the modification time of the file in the maildir with the given @p key.
+     * @since 4.7
+     */
+    QDateTime lastModified(const QString &key) const;
+
+    /**
+     * Return the contents of the file in the maildir with the given @p key.
+     */
+    QByteArray readEntry(const QString &key) const;
+
+    /**
+     * Return the flags encoded in the maildir file name for an entry
+     **/
+    Akonadi::Item::Flags readEntryFlags(const QString &key) const;
+
+    /**
+     * Return the contents of the headers section of the file the maildir with the given @p file, that
+     * is a full path to the file. You can get it by using findRealKey(key) .
+     */
+    QByteArray readEntryHeadersFromFile(const QString &file) const;
+
+    /**
+     * Return the contents of the headers section of the file the maildir with the given @p key.
+     */
+    QByteArray readEntryHeaders(const QString &key) const;
+
+    /**
+     * Write the given @p data to a file in the maildir with the given  @p key.
+     * Returns true in case of success, false in case of any error.
+     */
+    bool writeEntry(const QString &key, const QByteArray &data);
+
+    /**
+     * Adds the given @p data to the maildir. Returns the key of the entry.
+     */
+    QString addEntry(const QByteArray &data);
+
+    /**
+     * Removes the entry with the given @p key. Returns success or failure.
+     */
+    bool removeEntry(const QString &key);
+
+    /**
+     * Change the flags for an entry specified by @p key. Returns the new key of the entry (the key might change because
+     * flags are stored in the unique filename).
+     */
+    QString changeEntryFlags(const QString &key, const Akonadi::Item::Flags &flags);
+
+    /**
+     * Moves this maildir into @p destination.
+     */
+    bool moveTo(const Maildir &destination);
+
+    /**
+     * Renames this maildir to @p newName.
+     */
+    bool rename(const QString &newName);
+
+    /**
+     * Moves the file with the given @p key into the Maildir @p destination.
+     * @returns The new file name inside @p destination.
+     */
+    QString moveEntryTo(const QString &key, const KPIM::Maildir &destination);
+
+    /**
+     * Creates the maildir tree structure specific directory path that the
+     * given @p folderPath folder would have for its sub folders
+     * @param folderPath a maildir folder path
+     * @return the relative subDirPath for the given @p folderPath
+     *
+     * @see subDirNameForFolderName()
+     */
+    static QString subDirPathForFolderPath(const QString &folderPath);
+
+    /**
+     * Creates the maildir tree structure specific directory name that the
+     * given @p folderName folder would have for its sub folders
+     * @param folderName a maildir folder name
+     * @return the relative subDirName for the given @p folderMame
+     *
+     * @see subDirPathForFolderPath()
+     */
+    static QString subDirNameForFolderName(const QString &folderName);
+
+    /** Removes the listed keys from the key cache */
+    void removeCachedKeys(const QStringList &keys);
+
+    /** Reloads the keys associated with the maildir in the key cache*/
+    void refreshKeyCache();
+
+    /** Return the last error message string. The error might not come from the last performed operation,
+     if that was sucessful. The caller should always check the return value of the methods before
+     querying the last error string. */
+    QString lastError() const;
+
+private:
+    void swap(const Maildir &);
+    class Private;
+    Private *d;
+};
+
+}
+#endif // __MAILDIR_H__
diff --git a/resources/maildir/maildirresource.cpp b/resources/maildir/maildirresource.cpp
new file mode 100644 (file)
index 0000000..9db127e
--- /dev/null
@@ -0,0 +1,891 @@
+/*
+    Copyright (c) 2007 Till Adam <adam@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "maildirresource.h"
+#include "settings.h"
+#include "maildirsettingsadaptor.h"
+#include "configdialog.h"
+#include "retrieveitemsjob.h"
+
+#include <QtCore/QDir>
+#include <QtDBus/QDBusConnection>
+
+#include <Akonadi/KMime/MessageParts>
+#include <changerecorder.h>
+#include <itemfetchscope.h>
+#include <itemfetchjob.h>
+#include <itemmodifyjob.h>
+#include <collectionfetchscope.h>
+#include <cachepolicy.h>
+#include <collectionfetchjob.h>
+#include <kdbusconnectionpool.h>
+#include <Akonadi/KMime/MessageFlags>
+#include <kmime/kmime_message.h>
+
+#include "maildirresource_debug.h"
+#include <kdirwatch.h>
+#include <KLocalizedString>
+#include <kwindowsystem.h>
+
+#include "libmaildir/maildir.h"
+#include <QStandardPaths>
+
+using namespace Akonadi;
+using KPIM::Maildir;
+using namespace Akonadi_Maildir_Resource;
+
+#define CLEANER_TIMEOUT 2*6000
+
+Maildir MaildirResource::maildirForCollection(const Collection &col)
+{
+    const QString path = maildirPathForCollection(col);
+    if (mMaildirsForCollection.contains(path)) {
+        return mMaildirsForCollection.value(path);
+    }
+
+    if (col.remoteId().isEmpty()) {
+        qCWarning(MAILDIRRESOURCE_LOG) << "Got incomplete ancestor chain:" << col;
+        return Maildir();
+    }
+
+    if (col.parentCollection() == Collection::root()) {
+        if (col.remoteId() != mSettings->path()) {
+            qCWarning(MAILDIRRESOURCE_LOG) << "RID mismatch, is " << col.remoteId() << " expected " << mSettings->path();
+        }
+        Maildir maildir(col.remoteId(), mSettings->topLevelIsContainer());
+        mMaildirsForCollection.insert(path, maildir);
+        return maildir;
+    }
+    Maildir parentMd = maildirForCollection(col.parentCollection());
+    Maildir maildir = parentMd.subFolder(col.remoteId());
+    mMaildirsForCollection.insert(path, maildir);
+    return maildir;
+}
+
+Collection MaildirResource::collectionForMaildir(const Maildir &md) const
+{
+    if (!md.isValid()) {
+        return Collection();
+    }
+
+    Collection col;
+    if (md.path() == mSettings->path()) {
+        col.setRemoteId(md.path());
+        col.setParentCollection(Collection::root());
+    } else {
+        const Collection parent = collectionForMaildir(md.parent());
+        col.setRemoteId(md.name());
+        col.setParentCollection(parent);
+    }
+
+    return col;
+}
+
+MaildirResource::MaildirResource(const QString &id)
+    : ResourceBase(id),
+      mSettings(new MaildirSettings(config())),
+      mFsWatcher(new KDirWatch(this))
+{
+    // we cannot be sure that a config file is existing
+    // the MaildirResource will always be build
+    // look for a resource of this name
+    QString configFile = QStandardPaths::locate(QStandardPaths::ConfigLocation, id + QLatin1String("rc"));
+    // if not present, create it
+    if (configFile.isEmpty()) {
+        // check if the resource was used before
+        CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), Akonadi::CollectionFetchJob::FirstLevel, this);
+        job->fetchScope().setResource(id);
+        connect(job, &CollectionFetchJob::result, this, &MaildirResource::attemptConfigRestoring);
+        job->start();
+    }
+    new MaildirSettingsAdaptor(mSettings);
+    KDBusConnectionPool::threadConnection().registerObject(QStringLiteral("/Settings"),
+            mSettings, QDBusConnection::ExportAdaptors);
+    connect(this, &MaildirResource::reloadConfiguration, this, &MaildirResource::configurationChanged);
+
+    // We need to enable this here, otherwise we neither get the remote ID of the
+    // parent collection when a collection changes, nor the full item when an item
+    // is added.
+    changeRecorder()->fetchCollection(true);
+    changeRecorder()->itemFetchScope().fetchFullPayload(true);
+    changeRecorder()->itemFetchScope().setAncestorRetrieval(ItemFetchScope::All);
+    changeRecorder()->itemFetchScope().setFetchModificationTime(false);
+    changeRecorder()->collectionFetchScope().setAncestorRetrieval(CollectionFetchScope::All);
+    changeRecorder()->fetchChangedOnly(true);
+
+    setHierarchicalRemoteIdentifiersEnabled(true);
+
+    ItemFetchScope scope(changeRecorder()->itemFetchScope());
+    scope.fetchFullPayload(false);
+    scope.fetchPayloadPart(MessagePart::Header);
+    scope.setAncestorRetrieval(ItemFetchScope::None);
+    setItemSynchronizationFetchScope(scope);
+
+    connect(mFsWatcher, &KDirWatch::dirty, this, &MaildirResource::slotDirChanged);
+    if (!ensureSaneConfiguration()) {
+        Q_EMIT error(i18n("Unusable configuration."));
+    } else {
+        synchronizeCollectionTree();
+    }
+
+    mChangedCleanerTimer = new QTimer(this);
+    connect(mChangedCleanerTimer, &QTimer::timeout, this, &MaildirResource::changedCleaner);
+}
+
+void MaildirResource::attemptConfigRestoring(KJob *job)
+{
+    if (job->error()) {
+        qCDebug(MAILDIRRESOURCE_LOG) << job->errorString();
+        return;
+    }
+    // we cannot be sure that a config file is existing
+    const QString id = identifier();
+    const QString configFile = QStandardPaths::locate(QStandardPaths::ConfigLocation, id + QLatin1String("rc"));
+    // we test it again, to be sure
+    if (configFile.isEmpty()) {
+        // it is still empty, create it
+        qCWarning(MAILDIRRESOURCE_LOG) << "the resource is not properly configured:";
+        qCWarning(MAILDIRRESOURCE_LOG) << "there is no config file for the resource.";
+        qCWarning(MAILDIRRESOURCE_LOG) << "we create a new one.";
+        const Collection::List cols = qobject_cast<CollectionFetchJob *>(job)->collections();
+        QString path;
+        if (!cols.isEmpty()) {
+            qCDebug(MAILDIRRESOURCE_LOG) << "the collections list is not empty";
+            Collection col = cols.first();
+            // get the path of the collection
+            path = col.remoteId();
+        }
+        // test the path
+        if (path.isEmpty()) {
+            qCDebug(MAILDIRRESOURCE_LOG) << "build a new path";
+            const QString dataDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/');
+            // we use "id" to get an unique path
+            path = dataDir;
+            if (!defaultResourceType().isEmpty()) {
+                path += defaultResourceType()  + QLatin1Char('/');
+            }
+            path += id;
+            qCDebug(MAILDIRRESOURCE_LOG) << "set the path" << path;
+            mSettings->setPath(path);
+            // set the resource into container mode for its top level
+            mSettings->setTopLevelIsContainer(true);
+        } else {
+            // check how the directory looks like the actual check is missing.
+            Maildir root(mSettings->path(), true);
+            mSettings->setTopLevelIsContainer(root.isValid());
+        }
+        qCDebug(MAILDIRRESOURCE_LOG) << "synchronize";
+        configurationChanged();
+    }
+}
+
+MaildirResource::~ MaildirResource()
+{
+    delete mSettings;
+}
+
+bool MaildirResource::retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts)
+{
+    Q_UNUSED(parts);
+
+    const Maildir md = maildirForCollection(item.parentCollection());
+    if (!md.isValid()) {
+        cancelTask(i18n("Unable to fetch item: The maildir folder \"%1\" is not valid.",
+                        md.path()));
+        return false;
+    }
+
+    const QByteArray data = md.readEntry(item.remoteId());
+    KMime::Message *mail = new KMime::Message();
+    mail->setContent(KMime::CRLFtoLF(data));
+    mail->parse();
+
+    Item i(item);
+    i.setPayload(KMime::Message::Ptr(mail));
+    Akonadi::MessageFlags::copyMessageFlags(*mail, i);
+    itemRetrieved(i);
+    return true;
+}
+
+QString MaildirResource::itemMimeType() const
+{
+    return KMime::Message::mimeType();
+}
+
+void MaildirResource::configurationChanged()
+{
+    mSettings->save();
+    bool configValid = ensureSaneConfiguration();
+    configValid &= ensureDirExists();
+    if (configValid) {
+        Q_EMIT status(Idle);
+        setOnline(true);
+    }
+}
+
+void MaildirResource::aboutToQuit()
+{
+    // The settings may not have been saved if e.g. they have been modified via
+    // DBus instead of the config dialog.
+    mSettings->save();
+}
+
+QString MaildirResource::defaultResourceType()
+{
+    return QString();
+}
+
+void MaildirResource::configure(WId windowId)
+{
+    ConfigDialog dlg(mSettings, identifier());
+    if (windowId) {
+        KWindowSystem::setMainWindow(&dlg, windowId);
+    }
+    dlg.setWindowIcon(QIcon::fromTheme(QStringLiteral("message-rfc822")));
+    if (dlg.exec()) {
+        // if we have no name, or the default one,
+        // better use the name of the top level collection
+        // that looks nicer
+        if (name().isEmpty() || name() == identifier()) {
+            Maildir md(mSettings->path());
+            setName(md.name());
+        }
+        Q_EMIT configurationDialogAccepted();
+    } else {
+        Q_EMIT configurationDialogRejected();
+    }
+
+    configurationChanged();
+    synchronizeCollectionTree();
+}
+
+void MaildirResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection)
+{
+    if (!ensureSaneConfiguration()) {
+        cancelTask(i18n("Unusable configuration."));
+        return;
+    }
+    Maildir dir = maildirForCollection(collection);
+    if (mSettings->readOnly() || !dir.isValid()) {
+        cancelTask(dir.lastError());
+        return;
+    }
+
+    // we can only deal with mail
+    if (!item.hasPayload<KMime::Message::Ptr>()) {
+        cancelTask(i18n("Error: Unsupported type."));
+        return;
+    }
+    const KMime::Message::Ptr mail = item.payload<KMime::Message::Ptr>();
+
+    stopMaildirScan(dir);
+
+    const QString rid = dir.addEntry(mail->encodedContent());
+    mChangedFiles.insert(rid);
+    mChangedCleanerTimer->start(CLEANER_TIMEOUT);
+
+    if (rid.isEmpty()) {
+        restartMaildirScan(dir);
+        cancelTask(dir.lastError());
+        return;
+    }
+
+    restartMaildirScan(dir);
+
+    Item i(item);
+    i.setRemoteId(rid);
+    changeCommitted(i);
+}
+
+void MaildirResource::itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &parts)
+{
+    if (!ensureSaneConfiguration()) {
+        cancelTask(i18n("Unusable configuration."));
+        return;
+    }
+
+    bool bodyChanged = false;
+    bool flagsChanged = false;
+    bool headChanged = false;
+    Q_FOREACH (const QByteArray &part, parts)  {
+        if (part.startsWith("PLD:RFC822")) {
+            bodyChanged = true;
+        } else if (part.startsWith("PLD:HEAD")) {
+            headChanged = true;
+        }
+        if (part.contains("FLAGS")) {
+            flagsChanged = true;
+        }
+    }
+
+    if (mSettings->readOnly() || (!bodyChanged && !flagsChanged && !headChanged)) {
+        changeProcessed();
+        return;
+    }
+
+    Maildir dir = maildirForCollection(item.parentCollection());
+    if (!dir.isValid()) {
+        cancelTask(dir.lastError());
+        return;
+    }
+
+    Item newItem(item);
+
+    if (flagsChanged || bodyChanged || headChanged) {   //something has changed that we can deal with
+        stopMaildirScan(dir);
+
+        if (flagsChanged) {   //flags changed, store in file name and get back the new filename (id)
+            const QString newKey = dir.changeEntryFlags(item.remoteId(), item.flags());
+            if (newKey.isEmpty()) {
+                restartMaildirScan(dir);
+                cancelTask(i18n("Failed to change the flags for the mail. %1").arg(dir.lastError()));
+                return;
+            }
+            newItem.setRemoteId(newKey);
+        }
+
+        if (bodyChanged || headChanged) {   //head or body changed
+            // we can only deal with mail
+            if (item.hasPayload<KMime::Message::Ptr>()) {
+                const KMime::Message::Ptr mail = item.payload<KMime::Message::Ptr>();
+                QByteArray data = mail->encodedContent();
+                if (headChanged && !bodyChanged) {
+                    //only the head has changed, get the current version of the mail
+                    //replace the head and store the new mail in the file
+                    const QByteArray currentData = dir.readEntry(newItem.remoteId());
+                    if (currentData.isEmpty() && !dir.lastError().isEmpty()) {
+                        restartMaildirScan(dir);
+                        cancelTask(dir.lastError());
+                        return;
+                    }
+                    const QByteArray newHead = mail->head();
+                    mail->setContent(currentData);
+                    mail->setHead(newHead);
+                    mail->parse();
+                    data = mail->encodedContent();
+                }
+                if (!dir.writeEntry(newItem.remoteId(), data)) {
+                    restartMaildirScan(dir);
+                    cancelTask(dir.lastError());
+                    return;
+                }
+                mChangedFiles.insert(newItem.remoteId());
+                mChangedCleanerTimer->start(CLEANER_TIMEOUT);
+            } else {
+                restartMaildirScan(dir);
+                cancelTask(i18n("Maildir resource got a non-mail content."));
+                return;
+            }
+        }
+
+        restartMaildirScan(dir);
+
+        changeCommitted(newItem);
+    } else {
+        Q_EMIT changeProcessed();
+    }
+}
+
+void MaildirResource::itemMoved(const Item &item, const Collection &source, const Collection &destination)
+{
+    if (source == destination) {   // should not happen but would confuse Maildir::moveEntryTo
+        changeProcessed();
+        return;
+    }
+
+    if (!ensureSaneConfiguration()) {
+        cancelTask(i18n("Unusable configuration."));
+        return;
+    }
+
+    Maildir sourceDir = maildirForCollection(source);
+    if (!sourceDir.isValid()) {
+        cancelTask(i18n("Source folder is invalid: '%1'.", sourceDir.lastError()));
+        return;
+    }
+
+    Maildir destDir = maildirForCollection(destination);
+    if (!destDir.isValid()) {
+        cancelTask(i18n("Destination folder is invalid: '%1'.", destDir.lastError()));
+        return;
+    }
+
+    stopMaildirScan(sourceDir);
+    stopMaildirScan(destDir);
+
+    const QString newRid = sourceDir.moveEntryTo(item.remoteId(), destDir);
+
+    mChangedFiles.insert(newRid);
+    mChangedCleanerTimer->start(CLEANER_TIMEOUT);
+
+    restartMaildirScan(sourceDir);
+    restartMaildirScan(destDir);
+
+    if (newRid.isEmpty()) {
+        cancelTask(i18n("Could not move message '%1' from '%2' to '%3'. The error was %4.", item.remoteId(), sourceDir.path(), destDir.path(), sourceDir.lastError()));
+        return;
+    }
+
+    Item i(item);
+    i.setRemoteId(newRid);
+    changeCommitted(i);
+}
+
+void MaildirResource::itemRemoved(const Akonadi::Item &item)
+{
+    if (!ensureSaneConfiguration()) {
+        cancelTask(i18n("Unusable configuration."));
+        return;
+    }
+
+    if (!mSettings->readOnly()) {
+        Maildir dir = maildirForCollection(item.parentCollection());
+        // !dir.isValid() means that our parent folder has been deleted already,
+        // so we don't care at all as that one will be recursive anyway
+        stopMaildirScan(dir);
+        if (dir.isValid() && !dir.removeEntry(item.remoteId())) {
+            Q_EMIT error(i18n("Failed to delete message: %1", item.remoteId()));
+        }
+        restartMaildirScan(dir);
+    }
+    qCDebug(MAILDIRRESOURCE_LOG) << "Item removed" << item.id() << " in collection :" << item.parentCollection().id();
+    changeProcessed();
+}
+
+Collection::List MaildirResource::listRecursive(const Collection &root, const Maildir &dir)
+{
+    if (mSettings->monitorFilesystem()) {
+        mFsWatcher->addDir(dir.path() + QDir::separator() + QLatin1String("new"));
+        mFsWatcher->addDir(dir.path() + QDir::separator() + QLatin1String("cur"));
+        mFsWatcher->addDir(dir.subDirPath());
+        if (dir.isRoot()) {
+            mFsWatcher->addDir(dir.path());
+        }
+    }
+
+    Collection::List list;
+    const QStringList mimeTypes = QStringList() << itemMimeType() << Collection::mimeType();
+    foreach (const QString &sub, dir.subFolderList()) {
+        Collection c;
+        c.setName(sub);
+        c.setRemoteId(sub);
+        c.setParentCollection(root);
+        c.setContentMimeTypes(mimeTypes);
+
+        const Maildir md = maildirForCollection(c);
+        if (!md.isValid()) {
+            continue;
+        }
+
+        list << c;
+        list += listRecursive(c, md);
+    }
+    return list;
+}
+
+void MaildirResource::retrieveCollections()
+{
+    Maildir dir(mSettings->path(), mSettings->topLevelIsContainer());
+    if (!dir.isValid()) {
+        Q_EMIT error(dir.lastError());
+        collectionsRetrieved(Collection::List());
+        return;
+    }
+
+    Collection root;
+    root.setParentCollection(Collection::root());
+    root.setRemoteId(mSettings->path());
+    root.setName(name());
+    if (mSettings->readOnly()) {
+        root.setRights(Collection::ReadOnly);
+    } else {
+        if (mSettings->topLevelIsContainer()) {
+            root.setRights(Collection::ReadOnly | Collection::CanCreateCollection);
+        } else {
+            root.setRights(Collection::CanChangeItem | Collection::CanCreateItem | Collection::CanDeleteItem
+                           | Collection::CanCreateCollection);
+        }
+    }
+
+    CachePolicy policy;
+    policy.setInheritFromParent(false);
+    policy.setSyncOnDemand(true);
+    policy.setLocalParts(QStringList() << QLatin1String(MessagePart::Envelope));
+    policy.setCacheTimeout(1);
+    policy.setIntervalCheckTime(-1);
+    root.setCachePolicy(policy);
+
+    QStringList mimeTypes;
+    mimeTypes << Collection::mimeType();
+    mimeTypes << itemMimeType();
+    root.setContentMimeTypes(mimeTypes);
+
+    Collection::List list;
+    list << root;
+    list += listRecursive(root, dir);
+    collectionsRetrieved(list);
+}
+
+void MaildirResource::retrieveItems(const Akonadi::Collection &col)
+{
+    const Maildir md = maildirForCollection(col);
+    if (!md.isValid()) {
+        cancelTask(i18n("Maildir '%1' for collection '%2' is invalid.", md.path(), col.remoteId()));
+        return;
+    }
+
+    RetrieveItemsJob *job = new RetrieveItemsJob(col, md, this);
+    job->setMimeType(itemMimeType());
+    connect(job, &RetrieveItemsJob::result, this, &MaildirResource::slotItemsRetrievalResult);
+}
+
+void MaildirResource::slotItemsRetrievalResult(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(job->errorString());
+    } else {
+        itemsRetrievalDone();
+    }
+}
+
+void MaildirResource::collectionAdded(const Collection &collection, const Collection &parent)
+{
+    if (!ensureSaneConfiguration()) {
+        Q_EMIT error(i18n("Unusable configuration."));
+        changeProcessed();
+        return;
+    }
+
+    Maildir md = maildirForCollection(parent);
+    qCDebug(MAILDIRRESOURCE_LOG) << md.subFolderList();
+    if (mSettings->readOnly() || !md.isValid()) {
+        changeProcessed();
+        return;
+    } else {
+        const QString collectionName(collection.name().replace(QDir::separator(), QString()));
+        const QString newFolderPath = md.addSubFolder(collectionName);
+        if (newFolderPath.isEmpty()) {
+            changeProcessed();
+            return;
+        }
+
+        qCDebug(MAILDIRRESOURCE_LOG) << md.subFolderList();
+
+        Collection col = collection;
+        col.setRemoteId(collectionName);
+        col.setName(collectionName);
+        changeCommitted(col);
+    }
+
+}
+
+void MaildirResource::collectionChanged(const Collection &collection)
+{
+    if (!ensureSaneConfiguration()) {
+        Q_EMIT error(i18n("Unusable configuration."));
+        changeProcessed();
+        return;
+    }
+
+    if (collection.parentCollection() == Collection::root()) {
+        if (collection.name() != name()) {
+            setName(collection.name());
+        }
+        changeProcessed();
+        return;
+    }
+
+    if (collection.remoteId() == collection.name()) {
+        changeProcessed();
+        return;
+    }
+
+    Maildir md = maildirForCollection(collection);
+    if (!md.isValid()) {
+        assert(!collection.remoteId().isEmpty());   // caught in resourcebase
+        // we don't have a maildir for this collection yet, probably due to a race
+        // make one, otherwise the rename below will fail
+        md.create();
+    }
+
+    const QString collectionName(collection.name().replace(QDir::separator(), QString()));
+    if (!md.rename(collectionName)) {
+        Q_EMIT error(i18n("Unable to rename maildir folder '%1'.", collection.name()));
+        changeProcessed();
+        return;
+    }
+    Collection c(collection);
+    c.setRemoteId(collectionName);
+    c.setName(collectionName);
+    changeCommitted(c);
+}
+
+void MaildirResource::collectionMoved(const Collection &collection, const Collection &source, const Collection &dest)
+{
+    qCDebug(MAILDIRRESOURCE_LOG) << collection << source << dest;
+
+    if (!ensureSaneConfiguration()) {
+        Q_EMIT error(i18n("Unusable configuration."));
+        changeProcessed();
+        return;
+    }
+
+    if (collection.parentCollection() == Collection::root()) {
+        Q_EMIT error(i18n("Cannot move root maildir folder '%1'.", collection.remoteId()));
+        changeProcessed();
+        return;
+    }
+
+    if (source == dest) {   // should not happen, but who knows...
+        changeProcessed();
+        return;
+    }
+
+    Collection c(collection);
+    c.setParentCollection(source);
+    Maildir md = maildirForCollection(c);
+    Maildir destMd = maildirForCollection(dest);
+    if (!md.moveTo(destMd)) {
+        Q_EMIT error(i18n("Unable to move maildir folder '%1' from '%2' to '%3'.", collection.remoteId(), source.remoteId(), dest.remoteId()));
+        changeProcessed();
+    } else {
+        const QString path = maildirPathForCollection(c);
+        mMaildirsForCollection.remove(path);
+        changeCommitted(collection);
+    }
+}
+
+void MaildirResource::collectionRemoved(const Akonadi::Collection &collection)
+{
+    if (!ensureSaneConfiguration()) {
+        Q_EMIT error(i18n("Unusable configuration."));
+        changeProcessed();
+        return;
+    }
+
+    if (collection.parentCollection() == Collection::root()) {
+        Q_EMIT error(i18n("Cannot delete top-level maildir folder '%1'.", mSettings->path()));
+        changeProcessed();
+        return;
+    }
+
+    Maildir md = maildirForCollection(collection.parentCollection());
+    // !md.isValid() means that our parent folder has been deleted already,
+    // so we don't care at all as that one will be recursive anyway
+    if (md.isValid() && !md.removeSubFolder(collection.remoteId())) {
+        Q_EMIT error(i18n("Failed to delete sub-folder '%1'.", collection.remoteId()));
+    }
+
+    const QString path = maildirPathForCollection(collection);
+    mMaildirsForCollection.remove(path);
+
+    changeProcessed();
+}
+
+bool MaildirResource::ensureDirExists()
+{
+    Maildir root(mSettings->path());
+    if (!root.isValid(false) && !mSettings->topLevelIsContainer()) {
+        if (!root.create()) {
+            Q_EMIT status(Broken, i18n("Unable to create maildir '%1'.", mSettings->path()));
+        }
+        return false;
+    }
+    return true;
+}
+
+bool MaildirResource::ensureSaneConfiguration()
+{
+    if (mSettings->path().isEmpty()) {
+        Q_EMIT status(NotConfigured, i18n("No usable storage location configured."));
+        setOnline(false);
+        return false;
+    }
+    return true;
+}
+
+void MaildirResource::slotDirChanged(const QString &dir)
+{
+    QFileInfo fileInfo(dir);
+    if (fileInfo.isFile()) {
+        slotFileChanged(fileInfo);
+        return;
+    }
+
+    if (dir == mSettings->path()) {
+        synchronizeCollectionTree();
+        synchronizeCollection(Collection::root().id());
+        return;
+    }
+
+    if (dir.endsWith(QLatin1String(".directory"))) {
+        synchronizeCollectionTree(); //might be too much, but this is not a common case anyway
+        return;
+    }
+
+    QDir d(dir);
+    if (!d.cdUp()) {
+        return;
+    }
+
+    Maildir md(d.path());
+    if (!md.isValid()) {
+        return;
+    }
+
+    md.refreshKeyCache();
+
+    const Collection col = collectionForMaildir(md);
+    if (col.remoteId().isEmpty()) {
+        qCDebug(MAILDIRRESOURCE_LOG) << "unable to find collection for path" << dir;
+        return;
+    }
+
+    CollectionFetchJob *job = new CollectionFetchJob(col, Akonadi::CollectionFetchJob::Base, this);
+    connect(job, &CollectionFetchJob::result, this, &MaildirResource::fsWatchDirFetchResult);
+}
+
+void MaildirResource::fsWatchDirFetchResult(KJob *job)
+{
+    if (job->error()) {
+        qCDebug(MAILDIRRESOURCE_LOG) << job->errorString();
+        return;
+    }
+    const Collection::List cols = qobject_cast<CollectionFetchJob *>(job)->collections();
+    if (cols.isEmpty()) {
+        return;
+    }
+
+    synchronizeCollection(cols.first().id());
+}
+
+void MaildirResource::slotFileChanged(const QFileInfo &fileInfo)
+{
+    const QString key = fileInfo.fileName();
+    if (mChangedFiles.contains(key)) {
+        mChangedFiles.remove(key);
+        return;
+    }
+
+    QString path = fileInfo.path();
+    if (path.endsWith(QLatin1String("/new"))) {
+        path.remove(path.length() - 4, 4);
+    } else if (path.endsWith(QLatin1String("/cur"))) {
+        path.remove(path.length() - 4, 4);
+    }
+
+    const Maildir md(path);
+    if (!md.isValid()) {
+        return;
+    }
+
+    const Collection col = collectionForMaildir(md);
+    if (col.remoteId().isEmpty()) {
+        qCDebug(MAILDIRRESOURCE_LOG) << "unable to find collection for path" << fileInfo.path();
+        return;
+    }
+
+    Item item;
+    item.setRemoteId(key);
+    item.setParentCollection(col);
+
+    ItemFetchJob *job = new ItemFetchJob(item, this);
+    job->setProperty("entry", key);
+    job->setProperty("dir", path);
+    connect(job, &ItemFetchJob::result, this, &MaildirResource::fsWatchFileFetchResult);
+}
+
+void MaildirResource::fsWatchFileFetchResult(KJob *job)
+{
+    if (job->error()) {
+        qCDebug(MAILDIRRESOURCE_LOG) << job->errorString();
+        return;
+    }
+    Item::List items = qobject_cast<ItemFetchJob *>(job)->items();
+    if (items.isEmpty()) {
+        return;
+    }
+
+    const QString fileName = job->property("entry").toString();
+    const QString path = job->property("dir").toString();
+
+    const Maildir md(path);
+
+    QString entry = fileName;
+    Item item(items.at(0));
+    const qint64 entrySize = md.size(entry);
+    if (entrySize >= 0) {
+        item.setSize(entrySize);
+    }
+
+    Item::Flags flags = md.readEntryFlags(entry);
+    Q_FOREACH (const Item::Flag &flag, flags) {
+        item.setFlag(flag);
+    }
+
+    const QByteArray data = md.readEntry(entry);
+    KMime::Message *mail = new KMime::Message();
+    mail->setContent(KMime::CRLFtoLF(data));
+    mail->parse();
+
+    item.setPayload(KMime::Message::Ptr(mail));
+    Akonadi::MessageFlags::copyMessageFlags(*mail, item);
+
+    ItemModifyJob *mjob = new ItemModifyJob(item);
+    connect(mjob, &ItemModifyJob::result, this, &MaildirResource::fsWatchFileModifyResult);
+}
+
+void MaildirResource::fsWatchFileModifyResult(KJob *job)
+{
+    if (job->error()) {
+        qCDebug(MAILDIRRESOURCE_LOG) << job->errorString();
+        return;
+    }
+}
+
+QString MaildirResource::maildirPathForCollection(const Collection &collection) const
+{
+    QString path = collection.remoteId();
+    Akonadi::Collection parent = collection.parentCollection();
+    while (!parent.remoteId().isEmpty()) {
+        path.prepend(parent.remoteId() + QLatin1Char('/'));
+        parent = parent.parentCollection();
+    }
+
+    return path;
+}
+
+void MaildirResource::stopMaildirScan(const Maildir &maildir)
+{
+    const QString path = maildir.path();
+    mFsWatcher->stopDirScan(path + QLatin1Literal("/new"));
+    mFsWatcher->stopDirScan(path + QLatin1Literal("/cur"));
+}
+
+void MaildirResource::restartMaildirScan(const Maildir &maildir)
+{
+    const QString path = maildir.path();
+    mFsWatcher->restartDirScan(path + QLatin1Literal("/new"));
+    mFsWatcher->restartDirScan(path + QLatin1Literal("/cur"));
+}
+
+void MaildirResource::changedCleaner()
+{
+    mChangedFiles.clear();
+}
diff --git a/resources/maildir/maildirresource.desktop b/resources/maildir/maildirresource.desktop
new file mode 100644 (file)
index 0000000..e8c1fc8
--- /dev/null
@@ -0,0 +1,107 @@
+[Desktop Entry]
+Name=Maildir
+Name[ar]=Maildir
+Name[bg]=Maildir
+Name[bs]=Maildir
+Name[ca]=Maildir
+Name[ca@valencia]=Maildir
+Name[cs]=Maildir
+Name[da]=Maildir
+Name[de]=Maildir
+Name[el]=Maildir
+Name[en_GB]=Maildir
+Name[eo]=Maildir
+Name[es]=Maildir
+Name[et]=Maildir
+Name[fi]=Maildir
+Name[fr]=Maildir
+Name[ga]=Maildir
+Name[gl]=Maildir
+Name[hu]=Maildir
+Name[ia]=Maildir
+Name[it]=Maildir
+Name[ja]=Maildir
+Name[kk]=Maildir
+Name[km]=ថត​សំបុត្រ
+Name[ko]=Maildir
+Name[lt]=Maildir
+Name[lv]=Maildir
+Name[nb]=Maildir
+Name[nds]=Nettpostorner
+Name[nl]=Maildir
+Name[nn]=Maildir
+Name[pa]=Maildir
+Name[pl]=Maildir
+Name[pt]=Maildir
+Name[pt_BR]=Maildir
+Name[ro]=Maildir
+Name[ru]=Maildir
+Name[sk]=Maildir
+Name[sl]=MailDir
+Name[sq]=Maildir
+Name[sr]=Мејлдир
+Name[sr@ijekavian]=Мејлдир
+Name[sr@ijekavianlatin]=Maildir
+Name[sr@latin]=Maildir
+Name[sv]=Maildir
+Name[tr]=Maildir
+Name[uk]=Maildir
+Name[x-test]=xxMaildirxx
+Name[zh_CN]=邮件目录
+Name[zh_TW]=Maildir
+Comment=Loads data from a local maildir folder
+Comment[ar]=تحمل البيانات من مجلد maildir المحلي
+Comment[bg]=Зареждане на данни от локална папка maildir
+Comment[bs]=Učitava podatke iz lokalnog maildir direktorija
+Comment[ca]=Carrega les dades des d'una carpeta pel directori de correu local
+Comment[ca@valencia]=Carrega les dades des d'una carpeta pel directori de correu local
+Comment[cs]=Načítá data z místní složky maildir
+Comment[da]=Indlæser data fra en lokal maildir-mappe
+Comment[de]=Daten werden aus einem lokalen Maildir-Ordner geladen
+Comment[el]=Φόρτωσης δεδομένων από έναν τοπικό φάκελο maildir
+Comment[en_GB]=Loads data from a local maildir folder
+Comment[es]=Carga datos de una carpeta de maildir local
+Comment[et]=Andmete laadimine kohalikust maildir-kaustast
+Comment[fi]=Lataa tietoja paikallisesta maildir-kansiosta
+Comment[fr]=Charge des données d'un dossier au format « maildir »
+Comment[ga]=Breiseán a luchtaíonn sonraí ó fhillteán logánta maildir
+Comment[gl]=Carga datos desde un cartafol de correo local
+Comment[hu]=Adatbetöltő helyi maildir típusú mappákhoz
+Comment[ia]=Lege datos de un dossier local de Maildir
+Comment[it]=Carica dati da una cartella locale maildir
+Comment[ja]=ローカルの maildir フォルダからデータを読み込みます
+Comment[kk]=Жергілікті maildir қапшығынан деректі алып береді
+Comment[km]=ផ្ទុក​ទិន្នន័យ​ពី​ថត maildir មូលដ្ឋាន
+Comment[ko]=로컬 maildir 폴더에서 데이터를 가져옵니다
+Comment[lt]=Įkelia duomenis iš vietinio maildir aplanko
+Comment[lv]=Ielādē datus no lokālas maildir mapes
+Comment[nb]=Laster data fra en lokal maildir-mappe
+Comment[nds]=Laadt Daten ut en lokaal Nettpostorner
+Comment[nl]=Laadt gegevens van een lokaal maildir-map
+Comment[nn]=Lastar data frå ei lokal iCal-fil
+Comment[pa]=ਲੋਕਲ maildir ਫੋਲਡਰ ਤੋਂ ਡਾਟਾ ਲੋਡ ਕਰੋ
+Comment[pl]=Wczytuje dane z lokalnego folderu maildir
+Comment[pt]=Carrega os dados de uma pasta Maildir local
+Comment[pt_BR]=Carrega os dados de uma pasta maildir local
+Comment[ro]=Încarcă date dintr-un dosar maildir local
+Comment[ru]=Загрузка данных из локальной папки с почтой
+Comment[sk]=Načíta dáta z miestneho súboru maildir
+Comment[sl]=Naloži podatke iz krajevne poštne mape MailDir
+Comment[sr]=Учитава податке из локалне мејлдир фасцикле
+Comment[sr@ijekavian]=Учитава податке из локалне мејлдир фасцикле
+Comment[sr@ijekavianlatin]=Učitava podatke iz lokalne maildir fascikle
+Comment[sr@latin]=Učitava podatke iz lokalne maildir fascikle
+Comment[sv]=Laddar data från en lokal maildir-katalog
+Comment[tr]=Yerel maildir dizininden veri yükleme aracı
+Comment[uk]=Завантажує дані з локальної теки maildir
+Comment[x-test]=xxLoads data from a local maildir folderxx
+Comment[zh_CN]=从本地邮件目录载入数据
+Comment[zh_TW]=從本地 Maildir 格式的目錄中載入資料
+Type=AkonadiResource
+Exec=akonadi_maildir_resource
+Icon=message-rfc822
+
+X-Akonadi-MimeTypes=message/rfc822
+X-Akonadi-Capabilities=Resource
+X-Akonadi-Identifier=akonadi_maildir_resource
+X-Akonadi-Custom-HasLocalStorage=true
diff --git a/resources/maildir/maildirresource.h b/resources/maildir/maildirresource.h
new file mode 100644 (file)
index 0000000..76b864b
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+    Copyright (c) 2007 Till Adam <adam@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef MAILDIR_RESOURCE_H
+#define MAILDIR_RESOURCE_H
+
+#include <collection.h>
+#include <resourcebase.h>
+
+class QTimer;
+class QFileInfo;
+class KDirWatch;
+namespace Akonadi_Maildir_Resource
+{
+class MaildirSettings;
+}
+namespace KPIM
+{
+class Maildir;
+}
+
+class MaildirResource : public Akonadi::ResourceBase, public Akonadi::AgentBase::ObserverV2
+{
+    Q_OBJECT
+
+public:
+    MaildirResource(const QString &id);
+    ~MaildirResource();
+
+    virtual QString defaultResourceType();
+public Q_SLOTS:
+    void configure(WId windowId) Q_DECL_OVERRIDE;
+
+protected Q_SLOTS:
+    void retrieveCollections() Q_DECL_OVERRIDE;
+    void retrieveItems(const Akonadi::Collection &col) Q_DECL_OVERRIDE;
+    bool retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+
+protected:
+    virtual QString itemMimeType() const;
+
+    void aboutToQuit() Q_DECL_OVERRIDE;
+
+    void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    void itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+    void itemMoved(const Akonadi::Item &item, const Akonadi::Collection &source, const Akonadi::Collection &dest) Q_DECL_OVERRIDE;
+    void itemRemoved(const Akonadi::Item &item) Q_DECL_OVERRIDE;
+
+    void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) Q_DECL_OVERRIDE;
+    void collectionChanged(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    // do not hide the other variant, use implementation from base class
+    // which just forwards to the one above
+    using Akonadi::AgentBase::ObserverV2::collectionChanged;
+    void collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &source, const Akonadi::Collection &dest) Q_DECL_OVERRIDE;
+    void collectionRemoved(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void configurationChanged();
+    void slotItemsRetrievalResult(KJob *job);
+    void slotDirChanged(const QString &dir);
+    void slotFileChanged(const QFileInfo &fileInfo);
+    void fsWatchDirFetchResult(KJob *job);
+    void fsWatchFileFetchResult(KJob *job);
+    void fsWatchFileModifyResult(KJob *job);
+    // Try to restore some config values from Akonadi data
+    void attemptConfigRestoring(KJob *job);
+    void changedCleaner();
+
+private:
+    bool ensureDirExists();
+    bool ensureSaneConfiguration();
+    Akonadi::Collection::List listRecursive(const Akonadi::Collection &root, const KPIM::Maildir &dir);
+    /** Creates a maildir object for the collection @p col, given it has the full ancestor chain set. */
+    KPIM::Maildir maildirForCollection(const Akonadi::Collection &col);
+    /** Creates a collection object for the given maildir @p md. */
+    Akonadi::Collection collectionForMaildir(const KPIM::Maildir &md) const;
+
+    QString maildirPathForCollection(const Akonadi::Collection &collection) const;
+    void stopMaildirScan(const KPIM::Maildir &maildir);
+    void restartMaildirScan(const KPIM::Maildir &maildir);
+
+private:
+    Akonadi_Maildir_Resource::MaildirSettings *mSettings;
+    KDirWatch *mFsWatcher;
+    QHash<QString, KPIM::Maildir> mMaildirsForCollection;
+    QSet<QString> mChangedFiles; //files changed by the resource and that should be ignored in slotFileChanged
+    QTimer *mChangedCleanerTimer;
+};
+
+#endif
diff --git a/resources/maildir/maildirresource.kcfg b/resources/maildir/maildirresource.kcfg
new file mode 100644 (file)
index 0000000..5775fff
--- /dev/null
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:kcfg="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
+      http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
+  <kcfgfile arg="true"/>
+  <group name="General">
+    <entry name="Path" type="Path">
+      <label>Path to maildir</label>
+      <default>$HOME/.local/share/local-mail/</default>
+    </entry>
+    <entry name="TopLevelIsContainer" type="Bool">
+      <label>Path points to a folder containing Maildirs instead of to a maildir itself.</label>
+      <default>false</default>
+    </entry>
+    <entry name="ReadOnly" type="Bool">
+      <label>Do not change the actual backend data.</label>
+      <default>false</default>
+    </entry>
+    <entry name="MonitorFilesystem" type="Bool">
+      <default>true</default>
+    </entry>
+  </group>
+</kcfg>
diff --git a/resources/maildir/main.cpp b/resources/maildir/main.cpp
new file mode 100644 (file)
index 0000000..f92f865
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+    Copyright (c) 2015 Daniel Vrátil <dvratil@redhat.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "maildirresource.h"
+
+AKONADI_RESOURCE_MAIN(MaildirResource)
diff --git a/resources/maildir/retrieveitemsjob.cpp b/resources/maildir/retrieveitemsjob.cpp
new file mode 100644 (file)
index 0000000..b1d4b8b
--- /dev/null
@@ -0,0 +1,209 @@
+/*
+    Copyright (c) 2011 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "retrieveitemsjob.h"
+#include <itemfetchjob.h>
+#include <itemmodifyjob.h>
+#include <itemdeletejob.h>
+#include <itemcreatejob.h>
+#include <collectionmodifyjob.h>
+#include <transactionsequence.h>
+#include <Akonadi/KMime/MessageFlags>
+#include <AkonadiCore/vectorhelper.h>
+#include <QDirIterator>
+#include <KMime/Message>
+
+RetrieveItemsJob::RetrieveItemsJob(const Akonadi::Collection &collection, const KPIM::Maildir &md, QObject *parent) :
+    Job(parent),
+    m_collection(collection),
+    m_maildir(md),
+    m_mimeType(KMime::Message::mimeType()),
+    m_transaction(Q_NULLPTR),
+    m_transactionSize(0),
+    m_entryIterator(Q_NULLPTR),
+    m_previousMtime(0),
+    m_highestMtime(0)
+{
+    Q_ASSERT(m_collection.isValid());
+    Q_ASSERT(m_maildir.isValid());
+}
+
+void RetrieveItemsJob::setMimeType(const QString &mimeType)
+{
+    m_mimeType = mimeType;
+}
+
+void RetrieveItemsJob::doStart()
+{
+    Q_ASSERT(!m_mimeType.isEmpty());
+    Akonadi::ItemFetchJob *job = new Akonadi::ItemFetchJob(m_collection, this);
+    connect(job, &Akonadi::ItemFetchJob::result, this, &RetrieveItemsJob::localListDone);
+}
+
+void RetrieveItemsJob::localListDone(KJob *job)
+{
+    if (job->error()) {
+        return;    // handled by base class
+    }
+
+    const Akonadi::Item::List items = qobject_cast<Akonadi::ItemFetchJob *>(job)->items();
+    m_localItems.reserve(items.size());
+    Q_FOREACH (const Akonadi::Item &item, items) {
+        if (!item.remoteId().isEmpty()) {
+            m_localItems.insert(item.remoteId(), item);
+        }
+    }
+
+    m_listingPath = m_maildir.path() + QLatin1String("/new/");
+    delete m_entryIterator;
+    m_entryIterator = new QDirIterator(m_maildir.pathToNew(), QDir::Files);
+    m_previousMtime = m_collection.remoteRevision().toLongLong();
+    m_highestMtime = 0;
+    QMetaObject::invokeMethod(this, "processEntry", Qt::QueuedConnection);
+}
+
+void RetrieveItemsJob::processEntry()
+{
+    Akonadi::TransactionSequence *lastTrx = Q_NULLPTR;
+
+    while (m_entryIterator->hasNext() || m_listingPath.endsWith(QLatin1String("/new/"))) {
+        if (!m_entryIterator->hasNext()) {
+            m_listingPath = m_maildir.path() + QLatin1String("/cur/");
+            delete m_entryIterator;
+            m_entryIterator = new QDirIterator(m_maildir.pathToCurrent(), QDir::Files);
+            if (!m_entryIterator->hasNext()) {
+                break;
+            }
+        }
+        m_entryIterator->next();
+
+        const QFileInfo entryInfo = m_entryIterator->fileInfo();
+        const QString fileName = entryInfo.fileName();
+        const qint64 currentMtime = entryInfo.lastModified().toMSecsSinceEpoch();
+        m_highestMtime = qMax(m_highestMtime, currentMtime);
+        if (currentMtime <= m_previousMtime) {
+            auto localItemIter = m_localItems.find(fileName);
+            if (localItemIter != m_localItems.end()) {  // old, we got this one already
+                m_localItems.erase(localItemIter);
+                continue;
+            }
+        }
+
+        Akonadi::Item item;
+        item.setRemoteId(fileName);
+        item.setMimeType(m_mimeType);
+        const qint64 entrySize = entryInfo.size();
+        if (entrySize >= 0) {
+            item.setSize(entrySize);
+        }
+
+        KMime::Message *msg = new KMime::Message;
+        msg->setHead(KMime::CRLFtoLF(m_maildir.readEntryHeadersFromFile(m_listingPath + fileName)));
+        msg->parse();
+
+        Akonadi::Item::Flags flags = m_maildir.readEntryFlags(fileName);
+        Q_FOREACH (const Akonadi::Item::Flag &flag, flags) {
+            item.setFlag(flag);
+        }
+
+        item.setPayload(KMime::Message::Ptr(msg));
+        Akonadi::MessageFlags::copyMessageFlags(*msg, item);
+        auto localItemIter = m_localItems.find(fileName);
+        Akonadi::TransactionSequence *trx = transaction();
+        if (localItemIter == m_localItems.end()) { // new item
+            new Akonadi::ItemCreateJob(item, m_collection, trx);
+        } else { // modification
+            item.setId((*localItemIter).id());
+            new Akonadi::ItemModifyJob(item, trx);
+            m_localItems.erase(localItemIter);
+        }
+        if (trx != lastTrx) {
+            lastTrx = trx;
+            QMetaObject::invokeMethod(this, "processEntry", Qt::QueuedConnection);
+            return;
+        }
+    }
+
+    entriesProcessed();
+    //connect(job, &Akonadi::ItemCreateJob::result, this, &RetrieveItemsJob::processEntryDone);
+}
+
+void RetrieveItemsJob::processEntryDone(KJob *)
+{
+    processEntry();
+}
+
+void RetrieveItemsJob::entriesProcessed()
+{
+    delete m_entryIterator;
+    m_entryIterator = Q_NULLPTR;
+    if (!m_localItems.isEmpty()) {
+        Akonadi::ItemDeleteJob *job = new Akonadi::ItemDeleteJob(Akonadi::valuesToVector(m_localItems), transaction());
+        m_maildir.removeCachedKeys(m_localItems.keys());
+        // We ensure m_transaction is valid by calling transaction() above,
+        // however calling it again here could cause it to give us another transaction
+        // object (see transaction() comment for details)
+        m_transaction->setIgnoreJobFailure(job);
+    }
+
+    // update mtime
+    if (m_highestMtime != m_previousMtime) {
+        Akonadi::Collection newCol(m_collection);
+        newCol.setRemoteRevision(QString::number(m_highestMtime));
+        Akonadi::CollectionModifyJob *job = new Akonadi::CollectionModifyJob(newCol, transaction());
+        m_transaction->setIgnoreJobFailure(job);
+    }
+
+    if (!m_transaction) { // no jobs created here -> done
+        emitResult();
+    } else {
+        connect(m_transaction, &Akonadi::TransactionSequence::result,
+                this, &RetrieveItemsJob::transactionDone);
+        m_transaction->commit();
+    }
+}
+
+Akonadi::TransactionSequence *RetrieveItemsJob::transaction()
+{
+    // Commit transaction every 100 items, otherwise we are forcing server to
+    // hold the database transaction opened for potentially massive amount of
+    // operations, which slowly overloads the database journal causing simple
+    // INSERT to take several seconds
+    if (++m_transactionSize >= 100) {
+        qDebug() << "Commit!";
+        m_transaction->commit();
+        m_transaction = Q_NULLPTR;
+        m_transactionSize = 0;
+    }
+
+    if (!m_transaction) {
+        m_transaction = new Akonadi::TransactionSequence(this);
+        m_transaction->setAutomaticCommittingEnabled(false);
+    }
+    return m_transaction;
+}
+
+void RetrieveItemsJob::transactionDone(KJob *job)
+{
+    if (job->error()) {
+        return;    // handled by base class
+    }
+    emitResult();
+}
+
diff --git a/resources/maildir/retrieveitemsjob.h b/resources/maildir/retrieveitemsjob.h
new file mode 100644 (file)
index 0000000..1d07b59
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+    Copyright (c) 2011 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef MAILDIR_RETRIEVEITEMSJOB_H
+#define MAILDIR_RETRIEVEITEMSJOB_H
+
+#include <item.h>
+#include <job.h>
+#include <collection.h>
+
+#include "maildir.h"
+
+class QDirIterator;
+namespace Akonadi
+{
+class TransactionSequence;
+}
+
+/**
+ * Used to implement ResourceBase::retrieveItems() for Maildirs.
+ * This completely bypasses ItemSync in order to achieve maximum performance.
+ */
+class RetrieveItemsJob : public Akonadi::Job
+{
+    Q_OBJECT
+public:
+    RetrieveItemsJob(const Akonadi::Collection &collection, const KPIM::Maildir &md, QObject *parent = Q_NULLPTR);
+    void setMimeType(const QString &mimeType);
+
+protected:
+    void doStart() Q_DECL_OVERRIDE;
+
+private:
+    void entriesProcessed();
+    Akonadi::TransactionSequence *transaction();
+
+private Q_SLOTS:
+    void localListDone(KJob *job);
+    void transactionDone(KJob *job);
+    void processEntry();
+    void processEntryDone(KJob *);
+
+private:
+    Akonadi::Collection m_collection;
+    KPIM::Maildir m_maildir;
+    QHash<QString, Akonadi::Item> m_localItems;
+    QString m_mimeType;
+    Akonadi::TransactionSequence *m_transaction;
+    int m_transactionSize;
+    QDirIterator *m_entryIterator;
+    qint64 m_previousMtime;
+    qint64 m_highestMtime;
+    QString m_listingPath;
+};
+
+#endif
diff --git a/resources/maildir/settings.kcfgc b/resources/maildir/settings.kcfgc
new file mode 100644 (file)
index 0000000..69ccfef
--- /dev/null
@@ -0,0 +1,9 @@
+File=maildirresource.kcfg
+ClassName=MaildirSettings
+Mutators=true
+ItemAccessors=true
+SetUserTexts=true
+Singleton=false
+#IncludeFiles=
+GlobalEnums=true
+NameSpace=Akonadi_Maildir_Resource
diff --git a/resources/maildir/settings.ui b/resources/maildir/settings.ui
new file mode 100644 (file)
index 0000000..571dd15
--- /dev/null
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <author>Till Adam &lt;adam@kde.org&gt;</author>
+ <class>ConfigDialog</class>
+ <widget class="QWidget" name="ConfigDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>290</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Mail Directory Settings</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QTabWidget" name="tabWidget">
+     <property name="currentIndex">
+      <number>0</number>
+     </property>
+     <widget class="QWidget" name="tab">
+      <attribute name="title">
+       <string>Maildir</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_2">
+       <item>
+        <widget class="QLabel" name="label">
+         <property name="text">
+          <string>Select the folder containing the maildir information:</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="KUrlRequester" name="kcfg_Path"/>
+       </item>
+       <item>
+        <widget class="QCheckBox" name="kcfg_ReadOnly">
+         <property name="text">
+          <string>Open in read-only mode</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <spacer name="verticalSpacer">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>141</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="statusLabel">
+     <property name="text">
+      <string/>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KUrlRequester</class>
+   <extends>QFrame</extends>
+   <header>kurlrequester.h</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/resources/maildir/wizard/CMakeLists.txt b/resources/maildir/wizard/CMakeLists.txt
new file mode 100644 (file)
index 0000000..e8811cb
--- /dev/null
@@ -0,0 +1,2 @@
+
+install ( FILES maildirwizard.desktop maildirwizard.es maildirwizard.ui DESTINATION ${KDE_INSTALL_DATADIR}/akonadi/accountwizard/maildir )
diff --git a/resources/maildir/wizard/Messages.sh b/resources/maildir/wizard/Messages.sh
new file mode 100644 (file)
index 0000000..138740c
--- /dev/null
@@ -0,0 +1,4 @@
+#! /usr/bin/env bash
+$EXTRACTRC *.ui >> rc.cpp
+$XGETTEXT *.cpp -o $podir/accountwizard_maildir.pot
+$XGETTEXT -kqsTr *.es -j -o $podir/accountwizard_maildir.pot
diff --git a/resources/maildir/wizard/maildirwizard.desktop b/resources/maildir/wizard/maildirwizard.desktop
new file mode 100644 (file)
index 0000000..35ecf8f
--- /dev/null
@@ -0,0 +1,106 @@
+[Desktop Entry]
+Name=Maildir
+Name[ar]=Maildir
+Name[bg]=Maildir
+Name[bs]=Maildir
+Name[ca]=Maildir
+Name[ca@valencia]=Maildir
+Name[cs]=Maildir
+Name[da]=Maildir
+Name[de]=Maildir
+Name[el]=Maildir
+Name[en_GB]=Maildir
+Name[eo]=Maildir
+Name[es]=Maildir
+Name[et]=Maildir
+Name[fi]=Maildir
+Name[fr]=Maildir
+Name[ga]=Maildir
+Name[gl]=Maildir
+Name[hu]=Maildir
+Name[ia]=Maildir
+Name[it]=Maildir
+Name[ja]=Maildir
+Name[kk]=Maildir
+Name[km]=ថត​សំបុត្រ
+Name[ko]=Maildir
+Name[lt]=Maildir
+Name[lv]=Maildir
+Name[nb]=Maildir
+Name[nds]=Nettpostorner
+Name[nl]=Maildir
+Name[nn]=Maildir
+Name[pa]=Maildir
+Name[pl]=Maildir
+Name[pt]=Maildir
+Name[pt_BR]=Maildir
+Name[ro]=Maildir
+Name[ru]=Maildir
+Name[sk]=Maildir
+Name[sl]=MailDir
+Name[sq]=Maildir
+Name[sr]=Мејлдир
+Name[sr@ijekavian]=Мејлдир
+Name[sr@ijekavianlatin]=Maildir
+Name[sr@latin]=Maildir
+Name[sv]=Maildir
+Name[tr]=Maildir
+Name[uk]=Maildir
+Name[x-test]=xxMaildirxx
+Name[zh_CN]=邮件目录
+Name[zh_TW]=Maildir
+Icon=message-rfc822
+Comment=Maildir account
+Comment[bg]=Сметка Maildir
+Comment[bs]=Maildir nalog
+Comment[ca]=Compte Maildir
+Comment[ca@valencia]=Compte Maildir
+Comment[cs]=Maildir účet
+Comment[da]=Maildir-konto
+Comment[de]=Maildir-Zugang
+Comment[el]=Λογαριασμός Maildir
+Comment[en_GB]=Maildir account
+Comment[es]=Cuenta de Maildir
+Comment[et]=Maildir-konto
+Comment[fi]=Maildir-tili
+Comment[fr]=Compte Maildir
+Comment[ga]=Cuntas maildir
+Comment[gl]=Conta de Maildir
+Comment[hu]=Maildir azonosító
+Comment[ia]=Conto de Maildir
+Comment[it]=Account Maildir
+Comment[ja]=Maildir アカウント
+Comment[kk]=Maildir тіркелгісі
+Comment[km]=គណនី Maildir
+Comment[ko]=Maildir 계정
+Comment[lt]=Maildir paskyra
+Comment[lv]=Maildir konts
+Comment[nb]=Maildir-konto
+Comment[nds]=Nettpostkonto
+Comment[nl]=Maildir-account
+Comment[nn]=Maildir-konto
+Comment[pa]=Maildir ਅਕਾਊਂਟ
+Comment[pl]=Konto Maildir
+Comment[pt]=Conta Maildir
+Comment[pt_BR]=Conta Maildir
+Comment[ro]=Cont Maildir
+Comment[ru]=Учётная запись Maildir
+Comment[sk]=Účet poštového adresára
+Comment[sl]=Račun MailDir
+Comment[sr]=Мејлдир налог
+Comment[sr@ijekavian]=Мејлдир налог
+Comment[sr@ijekavianlatin]=Maildir nalog
+Comment[sr@latin]=Maildir nalog
+Comment[sv]=Maildir-konto
+Comment[tr]=Maildir hesabı
+Comment[uk]=Обліковий запис Maildir
+Comment[x-test]=xxMaildir accountxx
+Comment[zh_CN]=Maildir 账户
+Comment[zh_TW]=Maildir 帳號
+
+[Wizard]
+Type=message/rfc822
+Script=maildirwizard.es
+
+[Translate]
+Filename=accountwizard_maildir
diff --git a/resources/maildir/wizard/maildirwizard.es b/resources/maildir/wizard/maildirwizard.es
new file mode 100644 (file)
index 0000000..35ab385
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+    Copyright (c) 2009 Montel Laurent <montel@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+var page = Dialog.addPage( "maildirwizard.ui", qsTr("Personal Settings") );
+
+function validateInput()
+{
+  if ( page.widget().maildirPath.text == "" ) {
+    page.setValid( false );
+  } else {
+    page.setValid( true );
+  }
+}
+
+function setup()
+{
+  var maildirRes = SetupManager.createResource( "akonadi_maildir_resource" );
+  maildirRes.setOption( "Path", page.widget().maildirPath.text );
+
+  SetupManager.execute();
+}
+
+page.widget().maildirPath.textChanged.connect( validateInput );
+page.pageLeftNext.connect( setup );
+validateInput();
diff --git a/resources/maildir/wizard/maildirwizard.ui b/resources/maildir/wizard/maildirwizard.ui
new file mode 100644 (file)
index 0000000..c20efca
--- /dev/null
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>maildirWizard</class>
+ <widget class="QWidget" name="maildirWizard">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>300</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QLabel" name="label">
+       <property name="text">
+        <string>URL:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="KUrlRequester" name="maildirPath">
+       <property name="mode">
+        <set>KFile::Directory|KFile::ExistingOnly</set>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <spacer name="verticalSpacer_2">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>138</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KUrlRequester</class>
+   <extends>QFrame</extends>
+   <header>kurlrequester.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/resources/mbox/CMakeLists.txt b/resources/mbox/CMakeLists.txt
new file mode 100644 (file)
index 0000000..e9cc5ec
--- /dev/null
@@ -0,0 +1,52 @@
+
+add_subdirectory(wizard)
+
+########### next target ###############
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_mbox_resource\")
+
+set( mboxresource_SRCS
+  compactpage.cpp
+  lockmethodpage.cpp
+  deleteditemsattribute.cpp
+  mboxresource.cpp
+)
+
+ecm_qt_declare_logging_category(mboxresource_SRCS HEADER mboxresource_debug.h IDENTIFIER MBOXRESOURCE_LOG CATEGORY_NAME log_mboxresource)
+
+install( FILES mboxresource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents" )
+
+ki18n_wrap_ui(mboxresource_SRCS
+  compactpage.ui
+  lockfilepage.ui
+)
+kconfig_add_kcfg_files(mboxresource_SRCS settings.kcfgc)
+kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/mboxresource.kcfg org.kde.Akonadi.Mbox.Settings)
+qt5_add_dbus_adaptor(mboxresource_SRCS
+  ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.Mbox.Settings.xml settings.h Settings
+)
+
+add_executable(akonadi_mbox_resource ${mboxresource_SRCS})
+
+if( APPLE )
+  set_target_properties(akonadi_mbox_resource PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../Info.plist.template)
+  set_target_properties(akonadi_mbox_resource PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.Mbox")
+  set_target_properties(akonadi_mbox_resource PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi Mbox Resource")
+endif ()
+
+target_link_libraries(akonadi_mbox_resource
+    KF5::I18n
+    KF5::Mbox
+    KF5::AkonadiCore
+    KF5::AkonadiMime
+    KF5::KIOCore
+    KF5::Mime
+    KF5::AkonadiAgentBase
+    KF5::DBusAddons
+    KF5::KDELibs4Support
+    akonadi-singlefileresource
+)
+
+install(TARGETS akonadi_mbox_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
+if (BUILD_TESTING)
+   add_subdirectory(autotests)
+endif()
diff --git a/resources/mbox/Messages.sh b/resources/mbox/Messages.sh
new file mode 100644 (file)
index 0000000..efefc50
--- /dev/null
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+$EXTRACTRC `find . -name \*.ui` `find . -name \*.kcfg` >> rc.cpp || exit 11
+$XGETTEXT *.cpp -o $podir/akonadi_mbox_resource.pot
diff --git a/resources/mbox/autotests/CMakeLists.txt b/resources/mbox/autotests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..88969ee
--- /dev/null
@@ -0,0 +1,7 @@
+
+set( mbox_deleteitemsattributetest_source deleteitemsattributetest.cpp ../deleteditemsattribute.cpp)
+add_executable( deleteitemsattributetest ${mbox_deleteitemsattributetest_source})
+add_test(deleteitemsattributetest deleteitemsattributetest)
+ecm_mark_as_test(deleteitemsattributetest)
+target_link_libraries( deleteitemsattributetest Qt5::Test Qt5::Gui KF5::AkonadiCore KF5::Mbox)
+
diff --git a/resources/mbox/autotests/deleteitemsattributetest.cpp b/resources/mbox/autotests/deleteitemsattributetest.cpp
new file mode 100644 (file)
index 0000000..47e7f20
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+  Copyright (c) 2015-2016 Montel Laurent <montel@kde.org>
+
+  This program is free software; you can redistribute it and/or modify it
+  under the terms of the GNU General Public License, version 2, as
+  published by the Free Software Foundation.
+
+  This program is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#include "deleteitemsattributetest.h"
+#include "../deleteditemsattribute.h"
+#include <qtest.h>
+
+DeleteItemsAttributeTest::DeleteItemsAttributeTest(QObject *parent)
+    : QObject(parent)
+{
+
+}
+
+DeleteItemsAttributeTest::~DeleteItemsAttributeTest()
+{
+
+}
+
+void DeleteItemsAttributeTest::shouldHaveDefaultValue()
+{
+    DeletedItemsAttribute attr;
+    QVERIFY(attr.deletedItemOffsets().isEmpty());
+    QCOMPARE(attr.offsetCount(), 0);
+}
+
+void DeleteItemsAttributeTest::shouldAssignValue()
+{
+    DeletedItemsAttribute attr;
+    QSet<quint64> lst;
+    lst.insert(15);
+    attr.addDeletedItemOffset(15);
+    lst.insert(154);
+    attr.addDeletedItemOffset(154);
+    lst.insert(225);
+    attr.addDeletedItemOffset(225);
+    QVERIFY(!attr.deletedItemOffsets().isEmpty());
+    QCOMPARE(attr.offsetCount(), 3);
+    QCOMPARE(lst, attr.deletedItemOffsets());
+}
+
+void DeleteItemsAttributeTest::shouldDeserializeValue()
+{
+    DeletedItemsAttribute attr;
+    attr.addDeletedItemOffset(15);
+    attr.addDeletedItemOffset(154);
+    attr.addDeletedItemOffset(225);
+    const QByteArray ba = attr.serialized();
+    DeletedItemsAttribute result;
+    result.deserialize(ba);
+    QVERIFY(result == attr);
+}
+
+void DeleteItemsAttributeTest::shouldCloneAttribute()
+{
+    DeletedItemsAttribute attr;
+    attr.addDeletedItemOffset(15);
+    attr.addDeletedItemOffset(154);
+    attr.addDeletedItemOffset(225);
+    DeletedItemsAttribute *result = attr.clone();
+    QVERIFY(*result == attr);
+    delete result;
+}
+
+void DeleteItemsAttributeTest::shouldHaveTypeName()
+{
+    DeletedItemsAttribute attr;
+    QCOMPARE(attr.type(), QByteArray("DeletedMboxItems"));
+}
+
+QTEST_MAIN(DeleteItemsAttributeTest)
diff --git a/resources/mbox/autotests/deleteitemsattributetest.h b/resources/mbox/autotests/deleteitemsattributetest.h
new file mode 100644 (file)
index 0000000..f6c536a
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+  Copyright (c) 2015-2016 Montel Laurent <montel@kde.org>
+
+  This program is free software; you can redistribute it and/or modify it
+  under the terms of the GNU General Public License, version 2, as
+  published by the Free Software Foundation.
+
+  This program is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  General Public License for more details.
+
+  You should have received a copy of the GNU General Public License along
+  with this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+*/
+
+#ifndef DELETEITEMSATTRIBUTETEST_H
+#define DELETEITEMSATTRIBUTETEST_H
+
+#include <QObject>
+
+class DeleteItemsAttributeTest : public QObject
+{
+    Q_OBJECT
+public:
+    explicit DeleteItemsAttributeTest(QObject *parent = Q_NULLPTR);
+    ~DeleteItemsAttributeTest();
+
+private Q_SLOTS:
+    void shouldHaveDefaultValue();
+    void shouldDeserializeValue();
+    void shouldAssignValue();
+    void shouldCloneAttribute();
+    void shouldHaveTypeName();
+};
+
+#endif // DELETEITEMSATTRIBUTETEST_H
diff --git a/resources/mbox/compactpage.cpp b/resources/mbox/compactpage.cpp
new file mode 100644 (file)
index 0000000..f4348a2
--- /dev/null
@@ -0,0 +1,140 @@
+/*
+    Copyright (c) 2009 Bertjan Broeksema <broeksema@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "compactpage.h"
+
+#include <collectionfetchjob.h>
+#include <collectionmodifyjob.h>
+#include <kmbox/mbox.h>
+#include <KLocalizedString>
+#include <QUrl>
+
+#include "deleteditemsattribute.h"
+
+#include <QFileInfo>
+
+using namespace Akonadi;
+
+CompactPage::CompactPage(const QString &collectionId, QWidget *parent)
+    : QWidget(parent)
+    , mCollectionId(collectionId)
+{
+    ui.setupUi(this);
+
+    connect(ui.compactButton, &QPushButton::clicked, this, &CompactPage::compact);
+
+    checkCollectionId();
+}
+
+void CompactPage::checkCollectionId()
+{
+    if (!mCollectionId.isEmpty()) {
+        Collection collection;
+        collection.setRemoteId(mCollectionId);
+        CollectionFetchJob *fetchJob =
+            new CollectionFetchJob(collection, CollectionFetchJob::Base);
+
+        connect(fetchJob, &CollectionFetchJob::result, this, &CompactPage::onCollectionFetchCheck);
+    }
+}
+
+void CompactPage::compact()
+{
+    ui.compactButton->setEnabled(false);
+
+    Collection collection;
+    collection.setRemoteId(mCollectionId);
+    CollectionFetchJob *fetchJob =
+        new CollectionFetchJob(collection, CollectionFetchJob::Base);
+
+    connect(fetchJob, &CollectionFetchJob::result, this, &CompactPage::onCollectionFetchCompact);
+}
+
+void CompactPage::onCollectionFetchCheck(KJob *job)
+{
+    if (job->error()) {
+        // If we cannot fetch the collection, than also disable compacting.
+        ui.compactButton->setEnabled(false);
+        return;
+    }
+
+    CollectionFetchJob *fetchJob = dynamic_cast<CollectionFetchJob *>(job);
+    Q_ASSERT(fetchJob);
+    Q_ASSERT(fetchJob->collections().size() == 1);
+
+    Collection mboxCollection = fetchJob->collections().at(0);
+    DeletedItemsAttribute *attr
+        = mboxCollection.attribute<DeletedItemsAttribute>(Akonadi::Collection::AddIfMissing);
+
+    if (!attr->deletedItemOffsets().isEmpty()) {
+        ui.compactButton->setEnabled(true);
+        ui.messageLabel->setText(i18np("(1 message marked for deletion)",
+                                       "(%1 messages marked for deletion)", attr->deletedItemOffsets().size()));
+    }
+}
+
+void CompactPage::onCollectionFetchCompact(KJob *job)
+{
+    if (job->error()) {
+        ui.messageLabel->setText(i18n("Failed to fetch the collection."));
+        ui.compactButton->setEnabled(true);
+        return;
+    }
+
+    CollectionFetchJob *fetchJob = dynamic_cast<CollectionFetchJob *>(job);
+    Q_ASSERT(fetchJob);
+    Q_ASSERT(fetchJob->collections().size() == 1);
+
+    Collection mboxCollection = fetchJob->collections().at(0);
+    DeletedItemsAttribute *attr
+        = mboxCollection.attribute<DeletedItemsAttribute>(Akonadi::Collection::AddIfMissing);
+
+    KMBox::MBox mbox;
+    // TODO: Set lock method.
+    const QString fileName = QUrl::fromLocalFile(mCollectionId).toLocalFile();
+    if (!mbox.load(fileName)) {
+        ui.messageLabel->setText(i18n("Failed to load the mbox file"));
+    } else {
+        ui.messageLabel->setText(i18np("(Deleting 1 message)",
+                                       "(Deleting %1 messages)", attr->offsetCount()));
+        // TODO: implement and connect to messageProcessed signal.
+        if (mbox.purge(attr->deletedItemEntries()) ||
+                (QFileInfo(fileName).size() == 0)) {
+            // even if purge() failed but the file is now empty.
+            // it was probably deleted/emptied by an external prog. For whatever reason
+            // doesn't matter here. We know the file is empty so we can get rid
+            // of our stored DeletedItemsAttribute
+            mboxCollection.removeAttribute<DeletedItemsAttribute>();
+            CollectionModifyJob *modifyJob = new CollectionModifyJob(mboxCollection);
+            connect(modifyJob, &CollectionModifyJob::result, this, &CompactPage::onCollectionModify);
+        } else {
+            ui.messageLabel->setText(i18n("Failed to compact the mbox file."));
+        }
+    }
+}
+
+void CompactPage::onCollectionModify(KJob *job)
+{
+    if (job->error()) {
+        ui.messageLabel->setText(i18n("Failed to compact the mbox file."));
+    } else {
+        ui.messageLabel->setText(i18n("MBox file compacted."));
+    }
+}
+
diff --git a/resources/mbox/compactpage.h b/resources/mbox/compactpage.h
new file mode 100644 (file)
index 0000000..f0d0fa2
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+    Copyright (c) 2009 Bertjan Broeksema <broeksema@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef COMPACTPAGE_H
+#define COMPACTPAGE_H
+
+#include <QWidget>
+
+#include "ui_compactpage.h"
+
+class KJob;
+
+class CompactPage : public QWidget
+{
+    Q_OBJECT
+
+public:
+    explicit CompactPage(const QString &collectionId, QWidget *parent = Q_NULLPTR);
+
+private Q_SLOTS:
+    void compact();
+    void onCollectionFetchCheck(KJob *);
+    void onCollectionFetchCompact(KJob *);
+    void onCollectionModify(KJob *);
+
+private: // Methods
+    void checkCollectionId();
+
+private: // Members
+    QString mCollectionId;
+    Ui::CompactPage ui;
+};
+
+#endif
diff --git a/resources/mbox/compactpage.ui b/resources/mbox/compactpage.ui
new file mode 100644 (file)
index 0000000..80b11b5
--- /dev/null
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>CompactPage</class>
+ <widget class="QWidget" name="CompactPage">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>362</width>
+    <height>568</height>
+   </rect>
+  </property>
+  <layout class="QGridLayout" name="gridLayout_2">
+   <item row="0" column="0" colspan="4">
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Sans Serif'; font-size:10pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;The only way to fully remove a mail from an mbox file is by removing it from the actual file. As this can be a rather expensive operation, the mbox resource keeps a list of deleted messages. Once in a while these messages are really removed from the file.&lt;/p&gt;
+&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;/p&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Note:&lt;/span&gt; The downside of this is that if the file is changed by another program, the list of deleted messages cannot be trusted any longer and deleted messages might reappear.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="3" column="0">
+    <spacer name="horizontalSpacer">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>40</width>
+       <height>20</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+   <item row="3" column="2">
+    <widget class="QPushButton" name="compactButton">
+     <property name="enabled">
+      <bool>false</bool>
+     </property>
+     <property name="text">
+      <string>&amp;Compact now</string>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="2">
+    <spacer name="verticalSpacer">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>40</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+   <item row="3" column="1">
+    <widget class="QLabel" name="messageLabel">
+     <property name="text">
+      <string/>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="0" colspan="3">
+    <widget class="KButtonGroup" name="kcfg_CompactFrequency">
+     <layout class="QGridLayout" name="gridLayout_3">
+      <item row="1" column="0" colspan="2">
+       <widget class="QRadioButton" name="never">
+        <property name="text">
+         <string>&amp;Never compact automatically</string>
+        </property>
+       </widget>
+      </item>
+      <item row="2" column="0">
+       <widget class="QRadioButton" name="per_x_messages">
+        <property name="text">
+         <string>C&amp;ompact every</string>
+        </property>
+        <property name="checked">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item row="2" column="1">
+       <widget class="QSpinBox" name="kcfg_MessageCount">
+        <property name="suffix">
+         <string>msg</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KButtonGroup</class>
+   <extends>QGroupBox</extends>
+   <header>kbuttongroup.h</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>per_x_messages</sender>
+   <signal>toggled(bool)</signal>
+   <receiver>kcfg_MessageCount</receiver>
+   <slot>setEnabled(bool)</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>75</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>198</x>
+     <y>253</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
diff --git a/resources/mbox/deleteditemsattribute.cpp b/resources/mbox/deleteditemsattribute.cpp
new file mode 100644 (file)
index 0000000..f99c9d7
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+    Copyright (c) 2009 Bertjan Broeksema <broeksema@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "deleteditemsattribute.h"
+
+DeletedItemsAttribute::DeletedItemsAttribute()
+{
+}
+
+DeletedItemsAttribute::DeletedItemsAttribute(const DeletedItemsAttribute &other)
+    : Akonadi::Attribute()
+{
+    if (&other == this) {
+        return;
+    }
+
+    mDeletedItemOffsets = other.mDeletedItemOffsets;
+}
+
+DeletedItemsAttribute::~DeletedItemsAttribute()
+{
+}
+
+void DeletedItemsAttribute::addDeletedItemOffset(quint64 offset)
+{
+    mDeletedItemOffsets.insert(offset);
+}
+
+DeletedItemsAttribute *DeletedItemsAttribute::clone() const
+{
+    return new DeletedItemsAttribute(*this);
+}
+
+QSet<quint64> DeletedItemsAttribute::deletedItemOffsets() const
+{
+    return mDeletedItemOffsets;
+}
+
+KMBox::MBoxEntry::List DeletedItemsAttribute::deletedItemEntries() const
+{
+    KMBox::MBoxEntry::List entries;
+    entries.reserve(mDeletedItemOffsets.count());
+    foreach (quint64 offset, mDeletedItemOffsets) {
+        entries << KMBox::MBoxEntry(offset);
+    }
+
+    return entries;
+}
+
+void DeletedItemsAttribute::deserialize(const QByteArray &data)
+{
+    QList<QByteArray> offsets = data.split(',');
+    mDeletedItemOffsets.clear();
+
+    foreach (const QByteArray &offset, offsets) {
+        mDeletedItemOffsets.insert(offset.toULongLong());
+    }
+}
+
+QByteArray DeletedItemsAttribute::serialized() const
+{
+    QByteArray serialized;
+
+    foreach (quint64 offset, mDeletedItemOffsets) {
+        serialized += QByteArray::number(offset);
+        serialized += ',';
+    }
+
+    serialized.chop(1);   // Remove the last ','
+
+    return serialized;
+}
+
+int DeletedItemsAttribute::offsetCount() const
+{
+    return mDeletedItemOffsets.size();
+}
+
+QByteArray DeletedItemsAttribute::type() const
+{
+    static const QByteArray sType("DeletedMboxItems");
+    return sType;
+}
+
+bool DeletedItemsAttribute::operator==(const DeletedItemsAttribute &other) const
+{
+    return mDeletedItemOffsets == other.deletedItemOffsets();
+}
diff --git a/resources/mbox/deleteditemsattribute.h b/resources/mbox/deleteditemsattribute.h
new file mode 100644 (file)
index 0000000..e462a5c
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+    Copyright (c) 2009 Bertjan Broeksema <broeksema@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef DELETEDITEMSATTRIBUTE_H
+#define DELETEDITEMSATTRIBUTE_H
+
+#include <attribute.h>
+#include <kmbox/mboxentry.h>
+
+#include <QtCore/QSet>
+
+/**
+ * This attribute stores a list of offsets in the mbox file of mails which are
+ * deleted but not yet actually removed from the file yet.
+ */
+class DeletedItemsAttribute : public Akonadi::Attribute
+{
+public:
+    DeletedItemsAttribute();
+
+    DeletedItemsAttribute(const DeletedItemsAttribute &other);
+
+    ~DeletedItemsAttribute();
+
+    void addDeletedItemOffset(quint64);
+
+    DeletedItemsAttribute *clone() const Q_DECL_OVERRIDE;
+
+    QSet<quint64> deletedItemOffsets() const;
+    KMBox::MBoxEntry::List deletedItemEntries() const;
+
+    void deserialize(const QByteArray &data) Q_DECL_OVERRIDE;
+
+    /**
+     * Returns the number of offsets stored in this attribute.
+     */
+    int offsetCount() const;
+
+    QByteArray serialized() const Q_DECL_OVERRIDE;
+
+    QByteArray type() const Q_DECL_OVERRIDE;
+
+    bool operator ==(const DeletedItemsAttribute &other) const;
+private:
+    QSet<quint64> mDeletedItemOffsets;
+};
+
+#endif
+
diff --git a/resources/mbox/lockfilepage.ui b/resources/mbox/lockfilepage.ui
new file mode 100644 (file)
index 0000000..a07ad0c
--- /dev/null
@@ -0,0 +1,156 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <author>Bertjan Broeksema &lt;broeksema@kde.org&gt;</author>
+ <class>LockFilePage</class>
+ <widget class="QWidget" name="LockFilePage">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>317</width>
+    <height>369</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>MBox Settings</string>
+  </property>
+  <layout class="QGridLayout" name="gridLayout_2">
+   <item row="0" column="0" colspan="3">
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Sans Serif'; font-size:10pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Select a method to lock the mbox file when data is read from or written to the file.&lt;/p&gt;
+&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;/p&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Note&lt;/span&gt;: For some methods you might need to install additional software before they can be used.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="0" colspan="3">
+    <widget class="KButtonGroup" name="kcfg_LockfileMethod">
+     <property name="title">
+      <string/>
+     </property>
+     <layout class="QGridLayout" name="gridLayout">
+      <item row="0" column="0">
+       <widget class="QRadioButton" name="procmail">
+        <property name="text">
+         <string>Procmail loc&amp;kfile</string>
+        </property>
+        <property name="checked">
+         <bool>false</bool>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="0">
+       <widget class="QRadioButton" name="mutt_dotlock">
+        <property name="text">
+         <string>&amp;Mutt dotlock</string>
+        </property>
+       </widget>
+      </item>
+      <item row="2" column="0" colspan="2">
+       <widget class="QRadioButton" name="mutt_dotlock_privileged">
+        <property name="text">
+         <string>M&amp;utt dotlock privileged</string>
+        </property>
+       </widget>
+      </item>
+      <item row="3" column="0" colspan="2">
+       <widget class="QRadioButton" name="none">
+        <property name="text">
+         <string>Non&amp;e</string>
+        </property>
+        <property name="checked">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="1">
+       <widget class="KComboBox" name="kcfg_Lockfile">
+        <property name="enabled">
+         <bool>false</bool>
+        </property>
+        <property name="editable">
+         <bool>true</bool>
+        </property>
+        <item>
+         <property name="text">
+          <string>.lock</string>
+         </property>
+        </item>
+       </widget>
+      </item>
+      <item row="4" column="0" colspan="2">
+       <widget class="QLabel" name="label_2">
+        <property name="text">
+         <string>None, the default configuration, should be safe in most cases.  However, if programs that do not make use of Akonadi are also accessing the configured mbox file, you will need to set an appropriate locking method. Note that if this is the case, the resource and the other programs must all use the same locking method.</string>
+        </property>
+        <property name="wordWrap">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item row="2" column="1">
+    <spacer name="verticalSpacer">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>40</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KButtonGroup</class>
+   <extends>QGroupBox</extends>
+   <header>kbuttongroup.h</header>
+   <container>1</container>
+  </customwidget>
+  <customwidget>
+   <class>KComboBox</class>
+   <extends>QComboBox</extends>
+   <header>kcombobox.h</header>
+  </customwidget>
+ </customwidgets>
+ <tabstops>
+  <tabstop>procmail</tabstop>
+  <tabstop>kcfg_Lockfile</tabstop>
+  <tabstop>mutt_dotlock</tabstop>
+  <tabstop>mutt_dotlock_privileged</tabstop>
+  <tabstop>none</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>procmail</sender>
+   <signal>toggled(bool)</signal>
+   <receiver>kcfg_Lockfile</receiver>
+   <slot>setEnabled(bool)</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>64</x>
+     <y>141</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>211</x>
+     <y>140</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
diff --git a/resources/mbox/lockmethodpage.cpp b/resources/mbox/lockmethodpage.cpp
new file mode 100644 (file)
index 0000000..98d3f8c
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+    Copyright (c) 2009 Bertjan Broeksema <broeksema@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "lockmethodpage.h"
+#include "settings.h"
+
+#include <kconfigdialogmanager.h>
+
+#include <kurlrequester.h>
+#include <QStandardPaths>
+
+LockMethodPage::LockMethodPage(QWidget *parent) : QWidget(parent)
+{
+    ui.setupUi(this);
+    checkAvailableLockMethods();
+}
+
+void LockMethodPage::checkAvailableLockMethods()
+{
+    // FIXME: I guess this whole checking makes only sense on linux machines.
+
+    // Check for procmail lock method.
+    if (QStandardPaths::findExecutable(QStringLiteral("lockfile")).isEmpty()) {
+        ui.procmail->setEnabled(false);
+        if (ui.procmail->isChecked()) { // Select another lock method if necessary
+            ui.mutt_dotlock->setChecked(true);
+        }
+    }
+
+    // Check for mutt lock method.
+    if (QStandardPaths::findExecutable(QStringLiteral("mutt_dotlock")).isEmpty()) {
+        ui.mutt_dotlock->setEnabled(false);
+        ui.mutt_dotlock_privileged->setEnabled(false);
+        if (ui.mutt_dotlock->isChecked() || ui.mutt_dotlock_privileged->isChecked()) {
+            if (ui.procmail->isEnabled()) {
+                ui.procmail->setChecked(true);
+            } else {
+                ui.none->setChecked(true);
+            }
+        }
+    }
+}
+
diff --git a/resources/mbox/lockmethodpage.h b/resources/mbox/lockmethodpage.h
new file mode 100644 (file)
index 0000000..6420269
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+    Copyright (c) 2009 Bertjan Broeksema <broeksema@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef LOCKMETHODPAGE_H
+#define LOCKMETHODPAGE_H
+
+#include <QWidget>
+
+#include "ui_lockfilepage.h"
+
+class LockMethodPage : public QWidget
+{
+    Q_OBJECT
+public:
+    explicit LockMethodPage(QWidget *parent = Q_NULLPTR);
+
+private:
+    void checkAvailableLockMethods();
+
+private:
+    Ui::LockFilePage ui;
+};
+
+#endif
diff --git a/resources/mbox/mboxresource.cpp b/resources/mbox/mboxresource.cpp
new file mode 100644 (file)
index 0000000..7b4862f
--- /dev/null
@@ -0,0 +1,378 @@
+/*
+    Copyright (c) 2009 Bertjan Broeksem <broeksema@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "mboxresource.h"
+#include "mboxresource_debug.h"
+
+#include <QtCore/QtPlugin>
+
+#include <attributefactory.h>
+#include <changerecorder.h>
+#include <collectionfetchjob.h>
+#include <collectionmodifyjob.h>
+#include <kdbusconnectionpool.h>
+#include <itemfetchscope.h>
+#include <Akonadi/KMime/MessageFlags>
+#include <kmbox/mbox.h>
+#include <kmime/kmime_message.h>
+#include <KWindowSystem>
+#include <QIcon>
+
+#include "compactpage.h"
+#include "deleteditemsattribute.h"
+#include "lockmethodpage.h"
+#include "settingsadaptor.h"
+#include "singlefileresourceconfigdialog.h"
+
+using namespace Akonadi;
+
+static Collection::Id collectionId(const QString &remoteItemId)
+{
+    // [CollectionId]::[RemoteCollectionId]::[Offset]
+    Q_ASSERT(remoteItemId.split(QStringLiteral("::")).size() == 3);
+    return remoteItemId.split(QStringLiteral("::")).first().toLongLong();
+}
+
+static QString mboxFile(const QString &remoteItemId)
+{
+    // [CollectionId]::[RemoteCollectionId]::[Offset]
+    Q_ASSERT(remoteItemId.split(QStringLiteral("::")).size() == 3);
+    return remoteItemId.split(QStringLiteral("::")).at(1);
+}
+
+static quint64 itemOffset(const QString &remoteItemId)
+{
+    // [CollectionId]::[RemoteCollectionId]::[Offset]
+    Q_ASSERT(remoteItemId.split(QStringLiteral("::")).size() == 3);
+    return remoteItemId.split(QStringLiteral("::")).last().toULongLong();
+}
+
+MboxResource::MboxResource(const QString &id)
+    : SingleFileResource<Settings>(id)
+    , mMBox(Q_NULLPTR)
+{
+    new SettingsAdaptor(mSettings);
+    KDBusConnectionPool::threadConnection().registerObject(QStringLiteral("/Settings"),
+            mSettings, QDBusConnection::ExportAdaptors);
+
+    QStringList mimeTypes;
+    mimeTypes << QStringLiteral("message/rfc822");
+    setSupportedMimetypes(mimeTypes, QStringLiteral("message-rfc822"));
+
+    // Register the list of deleted items as an attribute of the collection.
+    AttributeFactory::registerAttribute<DeletedItemsAttribute>();
+}
+
+MboxResource::~MboxResource()
+{
+    delete mMBox;
+}
+
+void MboxResource::customizeConfigDialog(SingleFileResourceConfigDialog<Settings> *dlg)
+{
+    dlg->setWindowIcon(QIcon::fromTheme(QStringLiteral("message-rfc822")));
+    dlg->addPage(i18n("Compact frequency"), new CompactPage(mSettings->path()));
+    dlg->addPage(i18n("Lock method"), new LockMethodPage());
+    dlg->setWindowTitle(i18n("Select MBox file"));
+}
+
+void MboxResource::retrieveItems(const Akonadi::Collection &col)
+{
+    Q_UNUSED(col);
+    if (!mMBox) {
+        cancelTask();
+        return;
+    }
+    if (mMBox->fileName().isEmpty()) {
+        Q_EMIT status(NotConfigured, i18nc("@info:status", "MBox not configured."));
+        return;
+    }
+
+    reloadFile();
+
+    KMBox::MBoxEntry::List entryList;
+    if (col.hasAttribute<DeletedItemsAttribute>()) {
+        DeletedItemsAttribute *attr = col.attribute<DeletedItemsAttribute>();
+        entryList = mMBox->entries(attr->deletedItemEntries());
+    } else { // No deleted items (yet)
+        entryList = mMBox->entries();
+    }
+
+    mMBox->lock(); // Lock the file so that it doesn't get locked for every
+    // readEntryHeaders() call.
+
+    Item::List items;
+    QString colId = QString::number(col.id());
+    QString colRid = col.remoteId();
+    double count = 1;
+    const int entryListSize(entryList.size());
+    items.reserve(entryListSize);
+    foreach (const KMBox::MBoxEntry &entry, entryList) {
+        // TODO: Use cache policy to see what actually has to been set as payload.
+        //       Currently most views need a minimal amount of information so the
+        //       Items get Envelopes as payload.
+        KMime::Message *mail = new KMime::Message();
+        mail->setHead(KMime::CRLFtoLF(mMBox->readMessageHeaders(entry)));
+        mail->parse();
+
+        Item item;
+        item.setRemoteId(colId + QLatin1String("::") + colRid + QLatin1String("::") + QString::number(entry.messageOffset()));
+        item.setMimeType(QStringLiteral("message/rfc822"));
+        item.setSize(entry.messageSize());
+        item.setPayload(KMime::Message::Ptr(mail));
+        Akonadi::MessageFlags::copyMessageFlags(*mail, item);
+        Q_EMIT percent(count++ / entryListSize);
+        items << item;
+    }
+
+    mMBox->unlock(); // Now we have the items, unlock
+
+    itemsRetrieved(items);
+}
+
+bool MboxResource::retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts)
+{
+    Q_UNUSED(parts);
+
+    if (!mMBox) {
+        Q_EMIT error(i18n("MBox not loaded."));
+        return false;
+    }
+    if (mMBox->fileName().isEmpty()) {
+        Q_EMIT status(NotConfigured, i18nc("@info:status", "MBox not configured."));
+        return false;
+    }
+
+    const QString rid = item.remoteId();
+    const quint64 offset = itemOffset(rid);
+    KMime::Message *mail = mMBox->readMessage(KMBox::MBoxEntry(offset));
+    if (!mail) {
+        Q_EMIT error(i18n("Failed to read message with uid '%1'.", rid));
+        return false;
+    }
+
+    Item i(item);
+    i.setPayload(KMime::Message::Ptr(mail));
+    Akonadi::MessageFlags::copyMessageFlags(*mail, i);
+    itemRetrieved(i);
+    return true;
+}
+
+void MboxResource::aboutToQuit()
+{
+    if (!mSettings->readOnly()) {
+        writeFile();
+    }
+    mSettings->save();
+}
+
+void MboxResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection)
+{
+    if (!mMBox) {
+        cancelTask(i18n("MBox not loaded."));
+        return;
+    }
+    if (mMBox->fileName().isEmpty()) {
+        Q_EMIT status(NotConfigured, i18nc("@info:status", "MBox not configured."));
+        return;
+    }
+
+    // we can only deal with mail
+    if (!item.hasPayload<KMime::Message::Ptr>()) {
+        cancelTask(i18n("Only email messages can be added to the MBox resource."));
+        return;
+    }
+
+    const KMBox::MBoxEntry entry = mMBox->appendMessage(item.payload<KMime::Message::Ptr>());
+    if (!entry.isValid()) {
+        cancelTask(i18n("Mail message not added to the MBox."));
+        return;
+    }
+
+    scheduleWrite();
+    const QString rid = QString::number(collection.id()) + QLatin1String("::")
+                        + collection.remoteId() + QLatin1String("::") + QString::number(entry.messageOffset());
+
+    Item i(item);
+    i.setRemoteId(rid);
+
+    changeCommitted(i);
+}
+
+void MboxResource::itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &parts)
+{
+    if (parts.contains("PLD:RFC822")) {
+        qCDebug(MBOXRESOURCE_LOG) << itemOffset(item.remoteId());
+        // Only complete messages can be stored in a MBox file. Because all messages
+        // are stored in one single file we do an ItemDelete and an ItemCreate to
+        // prevent that whole file must been rewritten.
+        CollectionFetchJob *fetchJob =
+            new CollectionFetchJob(Collection(collectionId(item.remoteId()))
+                                   , CollectionFetchJob::Base);
+
+        connect(fetchJob, &CollectionFetchJob::result, this, &MboxResource::onCollectionFetch);
+
+        mCurrentItemDeletions.insert(fetchJob, item);
+
+        fetchJob->start();
+        return;
+    }
+
+    changeProcessed();
+}
+
+void MboxResource::itemRemoved(const Akonadi::Item &item)
+{
+    CollectionFetchJob *fetchJob =
+        new CollectionFetchJob(Collection(collectionId(item.remoteId()))
+                               , CollectionFetchJob::Base);
+
+    if (!fetchJob->exec()) {
+        cancelTask(i18n("Could not fetch the collection: %1", fetchJob->errorString()));
+        return;
+    }
+
+    Q_ASSERT(fetchJob->collections().size() == 1);
+    Collection mboxCollection = fetchJob->collections().at(0);
+    DeletedItemsAttribute *attr
+        = mboxCollection.attribute<DeletedItemsAttribute>(Akonadi::Collection::AddIfMissing);
+
+    if (mSettings->compactFrequency() == Settings::per_x_messages
+            && mSettings->messageCount() == static_cast<uint>(attr->offsetCount() + 1)) {
+        qCDebug(MBOXRESOURCE_LOG) << "Compacting mbox file";
+        mMBox->purge(attr->deletedItemEntries() << KMBox::MBoxEntry(itemOffset(item.remoteId())));
+        scheduleWrite();
+        mboxCollection.removeAttribute<DeletedItemsAttribute>();
+    } else {
+        attr->addDeletedItemOffset(itemOffset(item.remoteId()));
+    }
+
+    CollectionModifyJob *modifyJob = new CollectionModifyJob(mboxCollection);
+    if (!modifyJob->exec()) {
+        cancelTask(modifyJob->errorString());
+        return;
+    }
+
+    changeProcessed();
+}
+
+void MboxResource::handleHashChange()
+{
+    Q_EMIT warning(i18n("The MBox file was changed by another program. "
+                        "A copy of the new file was made and pending changes "
+                        "are appended to that copy. To prevent this from happening "
+                        "use locking and make sure that all programs accessing the mbox "
+                        "use the same locking method."));
+}
+
+bool MboxResource::readFromFile(const QString &fileName)
+{
+    delete mMBox;
+    mMBox = new KMBox::MBox();
+
+    switch (mSettings->lockfileMethod()) {
+    case Settings::procmail:
+        mMBox->setLockType(KMBox::MBox::ProcmailLockfile);
+        mMBox->setLockFile(mSettings->lockfile());
+        break;
+    case Settings::mutt_dotlock:
+        mMBox->setLockType(KMBox::MBox::MuttDotlock);
+        break;
+    case Settings::mutt_dotlock_privileged:
+        mMBox->setLockType(KMBox::MBox::MuttDotlockPrivileged);
+        break;
+    }
+
+    return mMBox->load(QUrl::fromLocalFile(fileName).toLocalFile());
+}
+
+bool MboxResource::writeToFile(const QString &fileName)
+{
+    if (!mMBox->save(fileName)) {
+        Q_EMIT error(i18n("Failed to save mbox file to %1", fileName));
+        return false;
+    }
+
+    // HACK: When writeToFile is called with another file than with which the mbox
+    // was loaded we assume that a backup is made as result of the fileChanged slot
+    // in SingleFileResourceBase. The problem is that SingleFileResource assumes that
+    // the implementing resources can save/retrieve the data from before the file
+    // change we have a problem at this point in the mbox resource. Therefore we
+    // copy the original file and append pending changes to it but also add an extra
+    // '\n' to make sure that the hashes differ and the user gets notified. Normally
+    // if this happens the user should make use of locking in all applications that
+    // use the mbox file.
+    if (fileName != mMBox->fileName()) {
+        QFile file(fileName);
+        file.open(QIODevice::WriteOnly);
+        file.seek(file.size());
+        file.write("\n");
+    }
+
+    return true;
+}
+
+/// Private slots
+
+void MboxResource::onCollectionFetch(KJob *job)
+{
+    Q_ASSERT(mCurrentItemDeletions.contains(job));
+    const Item item = mCurrentItemDeletions.take(job);
+
+    if (job->error()) {
+        cancelTask(job->errorString());
+        return;
+    }
+
+    CollectionFetchJob *fetchJob = dynamic_cast<CollectionFetchJob *>(job);
+    Q_ASSERT(fetchJob);
+    Q_ASSERT(fetchJob->collections().size() == 1);
+
+    Collection mboxCollection = fetchJob->collections().at(0);
+    DeletedItemsAttribute *attr
+        = mboxCollection.attribute<DeletedItemsAttribute>(Akonadi::Collection::AddIfMissing);
+    attr->addDeletedItemOffset(itemOffset(item.remoteId()));
+
+    CollectionModifyJob *modifyJob = new CollectionModifyJob(mboxCollection);
+    mCurrentItemDeletions.insert(modifyJob, item);
+    connect(modifyJob, &CollectionModifyJob::result, this, &MboxResource::onCollectionModify);
+    modifyJob->start();
+}
+
+void MboxResource::onCollectionModify(KJob *job)
+{
+    Q_ASSERT(mCurrentItemDeletions.contains(job));
+    const Item item = mCurrentItemDeletions.take(job);
+
+    if (job->error()) {
+        // Failed to store the offset of a deleted item in the DeletedItemsAttribute
+        // of the collection. In this case we shouldn't try to store the modified
+        // item.
+        cancelTask(i18n("Failed to update the changed item because the old item "
+                        "could not be deleted Reason: %1", job->errorString()));
+        return;
+    }
+
+    Collection c(collectionId(item.remoteId()));
+    c.setRemoteId(mboxFile(item.remoteId()));
+
+    itemAdded(item, c);
+}
+
+AKONADI_RESOURCE_MAIN(MboxResource)
diff --git a/resources/mbox/mboxresource.desktop b/resources/mbox/mboxresource.desktop
new file mode 100644 (file)
index 0000000..d30bb9d
--- /dev/null
@@ -0,0 +1,102 @@
+[Desktop Entry]
+Name=Mbox
+Name[bg]=Mbox
+Name[bs]=Mbox
+Name[ca]=Mbox
+Name[ca@valencia]=Mbox
+Name[cs]=MBox
+Name[da]=Mbox
+Name[de]=Mbox
+Name[el]=Mbox
+Name[en_GB]=Mbox
+Name[eo]=Mbox
+Name[es]=Mbox
+Name[et]=Mbox
+Name[fi]=Mbox
+Name[fr]=Mbox
+Name[ga]=Mbox
+Name[gl]=Mbox
+Name[hu]=Mbox
+Name[ia]=Mbox
+Name[it]=Mbox
+Name[ja]=Mbox
+Name[kk]=Mbox
+Name[km]=Mbox
+Name[ko]=Mbox
+Name[lt]=Mbox
+Name[lv]=Mbox
+Name[nb]=Mbox
+Name[nds]=MBox
+Name[nl]=Mbox
+Name[nn]=MBOX
+Name[pa]=Mbox
+Name[pl]=Mbox
+Name[pt]=Mbox
+Name[pt_BR]=Mbox
+Name[ro]=Mbox
+Name[ru]=MBox
+Name[sk]=Mbox
+Name[sl]=Mbox
+Name[sr]=Мбокс
+Name[sr@ijekavian]=Мбокс
+Name[sr@ijekavianlatin]=Mbox
+Name[sr@latin]=Mbox
+Name[sv]=Mbox
+Name[tr]=Mbox
+Name[uk]=Mbox
+Name[x-test]=xxMboxxx
+Name[zh_CN]=MBox
+Name[zh_TW]=Mbox
+Comment=Loads data from a local mbox file
+Comment[bg]=Зареждане на данни от локален файл mbox
+Comment[bs]=Učitava podatke iz lokalne mbox datoteke
+Comment[ca]=Carrega les dades des d'un fitxer «mbox» local
+Comment[ca@valencia]=Carrega les dades des d'un fitxer «mbox» local
+Comment[da]=Indlæser data fra en lokal mbox-fil
+Comment[de]=Daten werden aus einer lokalen MBox-Datei geladen
+Comment[el]=Φορτώνει δεδομένα από ένα τοπικό αρχείο mbox
+Comment[en_GB]=Loads data from a local mbox file
+Comment[es]=Carga datos de un archivo mbox local
+Comment[et]=Andmete laadimine kohalikust mbox-failist
+Comment[fi]=Lataa tietoa paikallisesta mbox-tiedostosta
+Comment[fr]=Charge des données depuis un fichier « mbox »
+Comment[ga]=Breiseán a luchtaíonn sonraí ó chomhad logánta mbox
+Comment[gl]=Carga datos desde un ficheiro mbox local
+Comment[hu]=Adatok betöltése egy helyi mbox fájlból
+Comment[ia]=Lege datos de un file local de Mbox
+Comment[it]=Carica dati da una cartella locale mbox
+Comment[ja]=ローカルの mbox ファイルからデータを読み込みます
+Comment[kk]=Жергілікті mbox файлынан деректі алып береді
+Comment[km]=ផ្ទុក​ទិន្នន័យ​ពី​ឯកសារ mbox មូលដ្ឋាន
+Comment[ko]=로컬 Mbox 파일에서 데이터를 불러옵니다
+Comment[lt]=įkelia duomenis iš vietinio mbox failo
+Comment[lv]=Ielādē datus no lokālā mbox faila
+Comment[nb]=Laster data fra en lokal mbox-fil
+Comment[nds]=Laadt Daten ut en lokaal Postfach (MBox-Datei)
+Comment[nl]=Laadt gegevens van een lokaal Mbox-bestand
+Comment[nn]=Lastar data frå ei lokal MBOX-fil
+Comment[pa]=ਲੋਕਲ mbox ਫਾਇਲ ਤੋਂ ਡਾਟਾ ਲੋਡ ਕਰੋ
+Comment[pl]=Wczytuje dane z pliku mbox
+Comment[pt]=Carrega os dados a partir de um ficheiro 'mbox' local
+Comment[pt_BR]=Carrega os dados de um arquivo mbox local
+Comment[ro]=Încarcă date dintr-un fișier mbox local
+Comment[ru]=Загрузка данных из локального файла MBox
+Comment[sk]=Načíta dáta z miestneho súboru mbox
+Comment[sl]=Naloži podatke iz krajevne datoteke Mbox
+Comment[sr]=Учитава податке из локалног мбокс фајла
+Comment[sr@ijekavian]=Учитава податке из локалног мбокс фајла
+Comment[sr@ijekavianlatin]=Učitava podatke iz lokalnog mbox fajla
+Comment[sr@latin]=Učitava podatke iz lokalnog mbox fajla
+Comment[sv]=Laddar data från en lokal mbox-fil
+Comment[tr]=Yerel mbox dosyasından veri yükler
+Comment[uk]=Завантажує дані з локального файла mbox
+Comment[x-test]=xxLoads data from a local mbox filexx
+Comment[zh_CN]=从本地 mbox 文件载入数据
+Comment[zh_TW]=從本地 mbox 檔載入資料
+Type=AkonadiResource
+Exec=akonadi_mbox_resource
+Icon=message-rfc822
+
+X-Akonadi-MimeTypes=message/rfc822
+X-Akonadi-Capabilities=Resource
+X-Akonadi-Identifier=akonadi_mbox_resource
diff --git a/resources/mbox/mboxresource.h b/resources/mbox/mboxresource.h
new file mode 100644 (file)
index 0000000..1cf624d
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+    Copyright (c) 2009 Bertjan Broeksem <broeksema@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef MBOX_RESOURCE_H
+#define MBOX_RESOURCE_H
+
+#include "settings.h"
+#include "singlefileresource.h"
+
+namespace KMBox
+{
+class MBox;
+}
+
+class MboxResource : public Akonadi::SingleFileResource<Settings>
+{
+    Q_OBJECT
+
+public:
+    explicit MboxResource(const QString &id);
+    ~MboxResource();
+
+protected Q_SLOTS:
+    bool retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+    void retrieveItems(const Akonadi::Collection &col) Q_DECL_OVERRIDE;
+
+protected:
+    void aboutToQuit() Q_DECL_OVERRIDE;
+
+    void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    void itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+    void itemRemoved(const Akonadi::Item &item) Q_DECL_OVERRIDE;
+
+    // From SingleFileResourceBase
+    void handleHashChange() Q_DECL_OVERRIDE;
+    bool readFromFile(const QString &fileName) Q_DECL_OVERRIDE;
+    bool writeToFile(const QString &fileName) Q_DECL_OVERRIDE;
+    /**
+     * Customize the configuration dialog before it is displayed.
+     */
+    void customizeConfigDialog(Akonadi::SingleFileResourceConfigDialog<Settings> *dlg) Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void onCollectionFetch(KJob *job);
+    void onCollectionModify(KJob *job);
+
+private:
+    QHash<KJob *, Akonadi::Item> mCurrentItemDeletions;
+    KMBox::MBox *mMBox;
+};
+
+#endif
diff --git a/resources/mbox/mboxresource.kcfg b/resources/mbox/mboxresource.kcfg
new file mode 100644 (file)
index 0000000..16c56fc
--- /dev/null
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:kcfg="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
+      http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
+  <kcfgfile arg="true" />
+  <group name="General">
+    <entry name="Path" type="Path">
+      <label>Path to mbox file.</label>
+      <default></default>
+    </entry>
+    <entry name="DisplayName" type="String">
+      <label>Display name.</label>
+      <default></default>
+    </entry>
+    <entry name="ReadOnly" type="Bool">
+      <label>Do not change the actual backend data.</label>
+      <default>false</default>
+    </entry>
+    <entry name="MonitorFile" type="Bool">
+      <label>Monitor file for changes.</label>
+      <default>true</default>
+    </entry>
+  </group>
+  <group name="Locking">
+    <entry name="LockfileMethod" type="Enum">
+      <choices>
+        <choice name="procmail"/>
+        <choice name="mutt_dotlock"/>
+        <choice name="mutt_dotlock_privileged"/>
+        <choice name="none"/>
+      </choices>
+      <default>none</default>
+    </entry>
+    <entry name="Lockfile" type="String">
+      <label>Lockfile</label>
+      <default></default>
+    </entry>
+  </group>
+  <group name="Compacting">
+    <entry name="CompactFrequency" type="Enum">
+      <choices>
+        <choice name="never"/>
+        <choice name="per_x_messages"/>
+      </choices>
+      <default>per_x_messages</default>
+    </entry>
+    <entry name="MessageCount" type="UInt">
+      <label>Number of deleted messages before a purge is started.</label>
+      <default>50</default>
+    </entry>
+  </group>
+</kcfg>
diff --git a/resources/mbox/settings.kcfgc b/resources/mbox/settings.kcfgc
new file mode 100644 (file)
index 0000000..f939b90
--- /dev/null
@@ -0,0 +1,8 @@
+File=mboxresource.kcfg
+ClassName=Settings
+Mutators=true
+ItemAccessors=true
+SetUserTexts=true
+Singleton=false
+#IncludeFiles=
+GlobalEnums=true
diff --git a/resources/mbox/wizard/CMakeLists.txt b/resources/mbox/wizard/CMakeLists.txt
new file mode 100644 (file)
index 0000000..dbf3fc1
--- /dev/null
@@ -0,0 +1,2 @@
+
+install ( FILES mailboxwizard.desktop mailboxwizard.es mailboxwizard.ui DESTINATION ${KDE_INSTALL_DATADIR}/akonadi/accountwizard/mailbox )
diff --git a/resources/mbox/wizard/Messages.sh b/resources/mbox/wizard/Messages.sh
new file mode 100644 (file)
index 0000000..fa2131b
--- /dev/null
@@ -0,0 +1,4 @@
+#! /usr/bin/env bash
+$EXTRACTRC *.ui >> rc.cpp
+$XGETTEXT *.cpp -o $podir/accountwizard_mailbox.pot
+$XGETTEXT -kqsTr *.es -j -o $podir/accountwizard_mailbox.pot
diff --git a/resources/mbox/wizard/mailboxwizard.desktop b/resources/mbox/wizard/mailboxwizard.desktop
new file mode 100644 (file)
index 0000000..e65e71e
--- /dev/null
@@ -0,0 +1,104 @@
+[Desktop Entry]
+Name=MailBox
+Name[bg]=MailBox
+Name[bs]=MailBox
+Name[ca]=Bústia
+Name[ca@valencia]=Bústia
+Name[cs]=Poštovní schránka
+Name[da]=MailBox
+Name[de]=Mailbox
+Name[el]=Γραμματοκιβώτιο
+Name[en_GB]=MailBox
+Name[es]=MailBox
+Name[et]=MailBox
+Name[fi]=MailBox
+Name[fr]=Messagerie
+Name[ga]=MailBox
+Name[gl]=Caixa de correo
+Name[hu]=MailBox
+Name[ia]=Cassa postal
+Name[it]=MailBox
+Name[ja]=メールボックス
+Name[kk]=MailBox
+Name[km]=MailBox
+Name[ko]=MailBox
+Name[lt]=Pašto dėžutė
+Name[lv]=MailBox
+Name[nb]=Postkasse
+Name[nds]=Postfach
+Name[nl]=MailBox
+Name[nn]=MailBox
+Name[pa]=MailBox
+Name[pl]=Skrzynka pocztowa
+Name[pt]=Caixa de Correio
+Name[pt_BR]=MailBox
+Name[ro]=MailBox
+Name[ru]=MailBox
+Name[sk]=MailBox
+Name[sl]=MailBox
+Name[sr]=Мбокс
+Name[sr@ijekavian]=Мбокс
+Name[sr@ijekavianlatin]=Mbox
+Name[sr@latin]=Mbox
+Name[sv]=Mailbox
+Name[tr]=MailBox
+Name[ug]=خەت ساندۇقى
+Name[uk]=MailBox
+Name[x-test]=xxMailBoxxx
+Name[zh_CN]=MailBox
+Name[zh_TW]=信箱格式(Mailbox)
+Icon=message-rfc822
+Comment=Mailbox account
+Comment[bg]=Сметка mailbox
+Comment[bs]=Mailbox nalog
+Comment[ca]=Compte de bústia
+Comment[ca@valencia]=Compte de bústia
+Comment[cs]=Účet poštovní schránky
+Comment[da]=Mailbox-konto
+Comment[de]=Mailbox-Zugang
+Comment[el]=Λογαριασμός Γραμματοκιβωτίου
+Comment[en_GB]=Mailbox account
+Comment[es]=Cuenta de Mailbox
+Comment[et]=Mailbox-konto
+Comment[fi]=Mailbox-tili
+Comment[fr]=Compte de messagerie
+Comment[ga]=Cuntas MailBox
+Comment[gl]=Conta da caixa de correo
+Comment[hu]=Mailbox azonosító
+Comment[ia]=Conto de cassa postal
+Comment[it]=Account Mailbox
+Comment[ja]=メールボックスのアカウント
+Comment[kk]=Mailbox тірккелгісі
+Comment[km]=គណនី Mailbox
+Comment[ko]=Mailbox 계정
+Comment[lt]=Pašto dėžutės paskyra
+Comment[lv]=Mailbox konts
+Comment[nb]=Mailbox-konto
+Comment[nds]=Nettpostkonto
+Comment[nl]=Mailbox-account
+Comment[nn]=MailBox-konto
+Comment[pa]=Mailbox ਅਕਾਊਂਟ
+Comment[pl]=Konto Mailbox
+Comment[pt]=Conta de Caixa do Correio
+Comment[pt_BR]=Conta Mailbox
+Comment[ro]=Cont Mailbox
+Comment[ru]=Учётная запись Maildir
+Comment[sk]=Účet mailboxu
+Comment[sl]=Račun MailBox
+Comment[sr]=Мбокс налог
+Comment[sr@ijekavian]=Мбокс налог
+Comment[sr@ijekavianlatin]=Mbox nalog
+Comment[sr@latin]=Mbox nalog
+Comment[sv]=Mailbox-konto
+Comment[tr]=Mailbox hesabı
+Comment[uk]=Обліковий запис Mailbox
+Comment[x-test]=xxMailbox accountxx
+Comment[zh_CN]=MailBox 账户
+Comment[zh_TW]=Mailbox 帳號
+
+[Wizard]
+Type=message/rfc822
+Script=mailboxwizard.es
+
+[Translate]
+Filename=accountwizard_mailbox
diff --git a/resources/mbox/wizard/mailboxwizard.es b/resources/mbox/wizard/mailboxwizard.es
new file mode 100644 (file)
index 0000000..287c337
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+    Copyright (c) 2010 Montel Laurent <montel@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+var page = Dialog.addPage( "mailboxwizard.ui", qsTr("Personal Settings") );
+
+function validateInput()
+{
+  if ( page.widget().mailboxPath.text == "" ) {
+    page.setValid( false );
+  } else {
+    page.setValid( true );
+  }
+}
+
+function setup()
+{
+  var mboxRes = SetupManager.createResource( "akonadi_mbox_resource" );
+  mboxRes.setOption( "Path", page.widget().mailboxPath.text );
+
+  SetupManager.execute();
+}
+
+page.widget().mailboxPath.textChanged.connect( validateInput );
+page.pageLeftNext.connect( setup );
+validateInput();
diff --git a/resources/mbox/wizard/mailboxwizard.ui b/resources/mbox/wizard/mailboxwizard.ui
new file mode 100644 (file)
index 0000000..032c384
--- /dev/null
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>mailboxWizard</class>
+ <widget class="QWidget" name="mailboxWizard">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>300</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QLabel" name="label">
+       <property name="text">
+        <string>URL:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="KUrlRequester" name="mailboxPath">
+       <property name="mode">
+        <set>KFile::ExistingOnly</set>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <spacer name="verticalSpacer_2">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>138</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KUrlRequester</class>
+   <extends>QFrame</extends>
+   <header>kurlrequester.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/resources/mixedmaildir/CMakeLists.txt b/resources/mixedmaildir/CMakeLists.txt
new file mode 100644 (file)
index 0000000..def5629
--- /dev/null
@@ -0,0 +1,69 @@
+project( mixedmaildirresource )
+
+include_directories(
+    ${kdepim-runtime_SOURCE_DIR}
+    ${kdepim-runtime_SOURCE_DIR}/resources/shared
+    ${kdepim-runtime_SOURCE_DIR}/resources/shared/filestore
+    ${kdepim-runtime_SOURCE_DIR}/resources/maildir
+    ${kdepim-runtime_SOURCE_DIR}/resources/mbox
+    ${CMAKE_CURRENT_SOURCE_DIR}/kmindexreader
+)
+
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_mixedmaildir_resource\")
+
+add_subdirectory(kmindexreader)
+
+########### next target ###############
+
+set( mixedmaildirresource_SRCS
+  mixedmaildirresource_debug.cpp
+  compactchangehelper.cpp
+  configdialog.cpp
+  mixedmaildirresource.cpp
+  mixedmaildirstore.cpp
+  retrieveitemsjob.cpp
+  mixedmaildir_debug.cpp
+)
+
+install( FILES mixedmaildirresource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents" )
+
+ki18n_wrap_ui(mixedmaildirresource_SRCS settings.ui)
+
+kconfig_add_kcfg_files(mixedmaildirresource_SRCS settings.kcfgc)
+
+kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/mixedmaildirresource.kcfg org.kde.Akonadi.MixedMaildir.Settings)
+
+qt5_add_dbus_adaptor(mixedmaildirresource_SRCS
+  ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.MixedMaildir.Settings.xml settings.h Settings
+)
+
+add_executable(akonadi_mixedmaildir_resource ${mixedmaildirresource_SRCS})
+
+if( APPLE )
+  set_target_properties(akonadi_mixedmaildir_resource PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../Info.plist.template)
+  set_target_properties(akonadi_mixedmaildir_resource PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.MixedMaildir")
+  set_target_properties(akonadi_mixedmaildir_resource PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi MixedMaildir Resource")
+endif ()
+
+target_link_libraries(akonadi_mixedmaildir_resource
+  kmindexreader
+  maildir
+  akonadi-filestore
+  KF5::AkonadiCore
+  KF5::AkonadiMime
+  KF5::KIOCore
+  KF5::Mbox
+  KF5::Mime
+  KF5::AkonadiAgentBase
+  KF5::KDELibs4Support
+  akonadi-singlefileresource
+)
+
+install(TARGETS akonadi_mixedmaildir_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.MixedMaildir.Settings.xml
+        DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR})
+
+# unit tests
+if (BUILD_TESTING)
+  add_subdirectory( autotests )
+endif ()
diff --git a/resources/mixedmaildir/Messages.sh b/resources/mixedmaildir/Messages.sh
new file mode 100644 (file)
index 0000000..207aa06
--- /dev/null
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+$EXTRACTRC *.ui *.kcfg >> rc.cpp
+$XGETTEXT *.cpp -o $podir/akonadi_mixedmaildir_resource.pot
diff --git a/resources/mixedmaildir/autotests/CMakeLists.txt b/resources/mixedmaildir/autotests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..9ae6c66
--- /dev/null
@@ -0,0 +1,205 @@
+include(ECMAddTests)
+
+find_package(Qt5Test CONFIG REQUIRED)
+
+if(${EXECUTABLE_OUTPUT_PATH})
+    set( PREVIOUS_EXEC_OUTPUT_PATH ${EXECUTABLE_OUTPUT_PATH} )
+else()
+    set( PREVIOUS_EXEC_OUTPUT_PATH . )
+endif()
+set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} )
+
+kde_enable_exceptions()
+
+include_directories(
+  ${CMAKE_CURRENT_SOURCE_DIR}/../
+  ${CMAKE_CURRENT_BINARY_DIR}/../
+  ${CMAKE_CURRENT_SOURCE_DIR}
+)
+
+# test data
+qt5_add_resources( testdata_generated_SRCS testdata.qrc )
+
+set( testdata_SRCS
+  ${testdata_generated_SRCS}
+  testdatautil.cpp
+  ../mixedmaildir_debug.cpp
+)
+
+remove_definitions(-DQT_NO_CAST_FROM_ASCII)
+
+add_executable(testdatatest testdatatest.cpp ${testdata_SRCS})
+add_test(testdatatest testdatatest)
+ecm_mark_as_test(mixedmaildir-testdatatest)
+
+target_link_libraries(
+  testdatatest
+  Qt5::Test
+)
+
+# put the libraries all tests link against into a variable and use it
+# in target_link_libraries instead
+set( common_link_libraries
+  kmindexreader
+  maildir
+  akonadi-filestore
+  KF5::AkonadiMime
+  KF5::AkonadiCore
+  KF5::Mbox
+  KF5::Mime
+  Qt5::Test
+)
+
+# test for overwritten methods
+add_executable(templatemethodstest ../mixedmaildirstore.cpp templatemethodstest.cpp ../mixedmaildir_debug.cpp ../mixedmaildirresource_debug.cpp)
+add_test(templatemethodstest templatemethodstest)
+ecm_mark_as_test(mixedmaildir-templatemethodtest)
+
+
+target_link_libraries(
+  templatemethodstest
+  ${common_link_libraries}
+)
+
+# test for collection creation handling
+add_executable(collectioncreatetest  ../mixedmaildirstore.cpp collectioncreatetest.cpp ../mixedmaildir_debug.cpp ../mixedmaildirresource_debug.cpp)
+add_test(collectioncreatetest collectioncreatetest)
+ecm_mark_as_test(mixedmaildir-collectioncreatetest)
+
+
+target_link_libraries(
+  collectioncreatetest
+  ${common_link_libraries}
+)
+
+# test for collection deletion handling
+add_executable(collectiondeletetest   ../mixedmaildirstore.cpp collectiondeletetest.cpp ../mixedmaildir_debug.cpp ../mixedmaildirresource_debug.cpp)
+add_test(collectiondeletetest collectiondeletetest)
+ecm_mark_as_test(mixedmaildir-collectiondeletetest)
+
+target_link_libraries(
+  collectiondeletetest
+  ${common_link_libraries}
+)
+
+# test for collection fetching handling
+add_executable( collectionfetchtest  ../mixedmaildirstore.cpp collectionfetchtest.cpp ../mixedmaildir_debug.cpp ../mixedmaildirresource_debug.cpp)
+add_test(collectionfetchtest collectionfetchtest)
+ecm_mark_as_test(mixedmaildir-collectionfetchtest)
+
+
+target_link_libraries(
+  collectionfetchtest
+  ${common_link_libraries}
+)
+
+# test for collection modification handling
+add_executable( collectionmodifytest  ../mixedmaildirstore.cpp
+ collectionmodifytest.cpp ../mixedmaildir_debug.cpp ../mixedmaildirresource_debug.cpp
+ ${testdata_SRCS}
+)
+add_test(collectionmodifytest collectionmodifytest)
+ecm_mark_as_test(mixedmaildir-collectionmodifytest)
+
+
+target_link_libraries(
+  collectionmodifytest
+  ${common_link_libraries}
+)
+
+# test for collection move handling
+add_executable( collectionmovetest  ../mixedmaildirstore.cpp ../mixedmaildir_debug.cpp
+ collectionmovetest.cpp ../mixedmaildirresource_debug.cpp
+ ${testdata_SRCS}
+)
+add_test(collectionmovetest collectionmovetest)
+ecm_mark_as_test(mixedmaildir-collectionmovetest)
+
+target_link_libraries(
+  collectionmovetest
+  ${common_link_libraries}
+)
+
+# test for item creation handling
+add_executable(itemcreatetest   ../mixedmaildirstore.cpp ../mixedmaildir_debug.cpp ../mixedmaildirresource_debug.cpp
+ itemcreatetest.cpp
+ ${testdata_SRCS}
+)
+add_test(itemcreatetest itemcreatetest)
+ecm_mark_as_test(mixedmaildir-itemcreatetest)
+
+
+target_link_libraries(
+  itemcreatetest
+  ${common_link_libraries}
+)
+
+# test for item creation handling
+add_executable(itemdeletetest  ../mixedmaildirstore.cpp
+ itemdeletetest.cpp ../mixedmaildirresource_debug.cpp
+ ${testdata_SRCS}
+)
+add_test(itemdeletetest itemdeletetest)
+ecm_mark_as_test(mixedmaildir-itemdeletetest)
+
+
+target_link_libraries(
+  itemdeletetest
+  ${common_link_libraries}
+)
+#REACTIVATE IT
+if (0)
+# test for item retrieval handling
+add_executable(itemfetchtest   ../mixedmaildirstore.cpp ../mixedmaildirresource_debug.cpp
+ itemfetchtest.cpp
+ ${testdata_SRCS}
+)
+add_test(itemfetchtest itemfetchtest)
+ecm_mark_as_test(mixedmaildir-itemfetchtest)
+
+target_link_libraries(
+  itemfetchtest
+  ${common_link_libraries}
+)
+endif()
+
+# test for item modification handling
+add_executable(itemmodifytest   ../mixedmaildirstore.cpp ../mixedmaildirresource_debug.cpp
+ itemmodifytest.cpp
+ ${testdata_SRCS}
+)
+add_test(itemmodifytest itemmodifytest)
+ecm_mark_as_test( mixedmaildir-itemmodifytest)
+
+target_link_libraries(
+  itemmodifytest
+  ${common_link_libraries}
+)
+
+# test for item move handling
+add_executable(itemmovetest  ../mixedmaildirstore.cpp ../mixedmaildirresource_debug.cpp
+ itemmovetest.cpp
+ ${testdata_SRCS}
+)
+add_test(itemmovetest itemmovetest)
+ecm_mark_as_test(mixedmaildir-itemmovetest)
+
+
+target_link_libraries(
+  itemmovetest
+  ${common_link_libraries}
+)
+
+# test for store compact handling
+add_executable(storecompacttest  ../mixedmaildirstore.cpp  ../mixedmaildirresource_debug.cpp
+ storecompacttest.cpp
+ ${testdata_SRCS}
+)
+add_test(storecompacttest storecompacttest)
+ecm_mark_as_test(mixedmaildir-storecompacttest)
+
+
+target_link_libraries(
+  storecompacttest
+  ${common_link_libraries}
+)
diff --git a/resources/mixedmaildir/autotests/collectioncreatetest.cpp b/resources/mixedmaildir/autotests/collectioncreatetest.cpp
new file mode 100644 (file)
index 0000000..60f9266
--- /dev/null
@@ -0,0 +1,330 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "mixedmaildirstore.h"
+
+#include "filestore/collectioncreatejob.h"
+
+#include "libmaildir/maildir.h"
+
+#include <kmime/kmime_message.h>
+
+#include <QTemporaryDir>
+
+#include <qtest.h>
+#include <QFileInfo>
+#include <QDir>
+
+using namespace Akonadi;
+
+class CollectionCreateTest : public QObject
+{
+    Q_OBJECT
+
+public:
+    CollectionCreateTest() : QObject(), mStore(0), mDir(0) {}
+    ~CollectionCreateTest()
+    {
+        delete mStore;
+        delete mDir;
+    }
+
+private:
+    MixedMaildirStore *mStore;
+    QTemporaryDir *mDir;
+
+private Q_SLOTS:
+    void init();
+    void cleanup();
+    void testCollectionProperties();
+    void testEmptyDir();
+    void testMaildirTree();
+    void testMixedTree();
+};
+
+void CollectionCreateTest::init()
+{
+    mStore = new MixedMaildirStore;
+
+    mDir = new QTemporaryDir;
+    QVERIFY(mDir->isValid());
+    QVERIFY(QDir(mDir->path()).exists());
+
+}
+
+void CollectionCreateTest::cleanup()
+{
+    delete mStore;
+    mStore = 0;
+    delete mDir;
+    mDir = 0;
+}
+
+void CollectionCreateTest::testCollectionProperties()
+{
+    mStore->setPath(mDir->path());
+
+    FileStore::CollectionCreateJob *job = 0;
+
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    job = mStore->createCollection(collection1, mStore->topLevelCollection());
+    QVERIFY(job != 0);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection1 = job->collection();
+    QCOMPARE(collection1.remoteId(), collection1.name());
+
+    QCOMPARE(collection1.contentMimeTypes(), QStringList() << Collection::mimeType() << KMime::Message::mimeType());
+
+    QCOMPARE(collection1.rights(), Collection::CanCreateItem |
+             Collection::CanChangeItem |
+             Collection::CanDeleteItem |
+             Collection::CanCreateCollection |
+             Collection::CanChangeCollection |
+             Collection::CanDeleteCollection);
+}
+
+void CollectionCreateTest::testEmptyDir()
+{
+    mStore->setPath(mDir->path());
+
+    KPIM::Maildir topLevelMd(mStore->path(), true);
+
+    FileStore::CollectionCreateJob *job = 0;
+
+    // test creating first level collections
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    job = mStore->createCollection(collection1, mStore->topLevelCollection());
+    QVERIFY(job != 0);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection1 = job->collection();
+    QVERIFY(!collection1.remoteId().isEmpty());
+    QVERIFY(collection1.parentCollection() == mStore->topLevelCollection());
+
+    QCOMPARE(topLevelMd.subFolderList(), QStringList() << QStringLiteral("collection1"));
+    KPIM::Maildir md1 = topLevelMd.subFolder(collection1.remoteId());
+    QVERIFY(md1.isValid());
+
+    Collection collection2;
+    collection2.setName(QStringLiteral("collection2"));
+    job = mStore->createCollection(collection2, mStore->topLevelCollection());
+    QVERIFY(job != 0);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection2 = job->collection();
+    QVERIFY(!collection2.remoteId().isEmpty());
+    QVERIFY(collection2.parentCollection() == mStore->topLevelCollection());
+
+    QCOMPARE(topLevelMd.subFolderList(), QStringList() << QStringLiteral("collection1") << QStringLiteral("collection2"));
+    KPIM::Maildir md2 = topLevelMd.subFolder(collection2.remoteId());
+    QVERIFY(md2.isValid());
+
+    // test creating second level collections
+    Collection collection1_1;
+    collection1_1.setName(QStringLiteral("collection1_1"));
+    job = mStore->createCollection(collection1_1, collection1);
+    QVERIFY(job != 0);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection1_1 = job->collection();
+    QVERIFY(!collection1_1.remoteId().isEmpty());
+    QVERIFY(collection1_1.parentCollection() == collection1);
+
+    QCOMPARE(md1.subFolderList(), QStringList() << QStringLiteral("collection1_1"));
+    KPIM::Maildir md1_1 = md1.subFolder(collection1_1.remoteId());
+    QVERIFY(md1_1.isValid());
+
+    Collection collection1_2;
+    collection1_2.setName(QStringLiteral("collection1_2"));
+    job = mStore->createCollection(collection1_2, collection1);
+    QVERIFY(job != 0);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection1_2 = job->collection();
+    QVERIFY(!collection1_2.remoteId().isEmpty());
+    QVERIFY(collection1_2.parentCollection() == collection1);
+
+    QCOMPARE(md1.subFolderList(), QStringList() << QStringLiteral("collection1_1") << QStringLiteral("collection1_2"));
+    KPIM::Maildir md1_2 = md1.subFolder(collection1_2.remoteId());
+    QVERIFY(md1_2.isValid());
+
+    QCOMPARE(md2.subFolderList(), QStringList());
+}
+
+void CollectionCreateTest::testMaildirTree()
+{
+    KPIM::Maildir topLevelMd(mDir->path(), true);
+    QVERIFY(topLevelMd.isValid());
+
+    KPIM::Maildir md1(topLevelMd.addSubFolder(QStringLiteral("collection1")), false);
+
+    KPIM::Maildir md1_2(md1.addSubFolder(QStringLiteral("collection1_2")), false);
+
+    mStore->setPath(mDir->path());
+
+    FileStore::CollectionCreateJob *job = 0;
+
+    // test creating first level collections
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    job = mStore->createCollection(collection1, mStore->topLevelCollection());
+    QVERIFY(job != 0);
+
+    QVERIFY(job->exec());   // works because it already exists
+    QCOMPARE(job->error(), 0);
+
+    collection1 = job->collection();
+    QVERIFY(!collection1.remoteId().isEmpty());
+    QVERIFY(collection1.parentCollection() == mStore->topLevelCollection());
+
+    Collection collection2;
+    collection2.setName(QStringLiteral("collection2"));
+    job = mStore->createCollection(collection2, mStore->topLevelCollection());
+    QVERIFY(job != 0);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection2 = job->collection();
+    QVERIFY(!collection2.remoteId().isEmpty());
+    QVERIFY(collection2.parentCollection() == mStore->topLevelCollection());
+
+    QCOMPARE(topLevelMd.subFolderList(), QStringList() << QStringLiteral("collection1") << QStringLiteral("collection2"));
+    KPIM::Maildir md2 = topLevelMd.subFolder(collection2.remoteId());
+    QVERIFY(md2.isValid());
+
+    // test creating second level collections
+    Collection collection1_1;
+    collection1_1.setName(QStringLiteral("collection1_1"));
+    job = mStore->createCollection(collection1_1, collection1);
+    QVERIFY(job != 0);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection1_1 = job->collection();
+    QVERIFY(!collection1_1.remoteId().isEmpty());
+    QCOMPARE(collection1_1.parentCollection().remoteId(), QStringLiteral("collection1"));
+
+    QCOMPARE(md1.subFolderList(), QStringList() << QStringLiteral("collection1_1") << QStringLiteral("collection1_2"));
+    KPIM::Maildir md1_1 = md1.subFolder(collection1_1.remoteId());
+    QVERIFY(md1_1.isValid());
+
+    Collection collection1_2;
+    collection1_2.setName(QStringLiteral("collection1_2"));
+    job = mStore->createCollection(collection1_2, collection1);
+    QVERIFY(job != 0);
+
+    QVERIFY(job->exec());   // works because it already exists
+    QCOMPARE(job->error(), 0);
+
+    collection1_2 = job->collection();
+    QVERIFY(!collection1_2.remoteId().isEmpty());
+    QCOMPARE(collection1_2.parentCollection().remoteId(), QStringLiteral("collection1"));
+
+    QCOMPARE(md2.subFolderList(), QStringList());
+}
+
+void CollectionCreateTest::testMixedTree()
+{
+    KPIM::Maildir topLevelMd(mDir->path(), true);
+    QVERIFY(topLevelMd.isValid());
+
+    // simulate a first level MBox
+    QFileInfo fileInfo1(mDir->path(), QStringLiteral("collection1"));
+    QFile file1(fileInfo1.absoluteFilePath());
+    file1.open(QIODevice::WriteOnly);
+    file1.close();
+    QVERIFY(fileInfo1.exists());
+
+    mStore->setPath(mDir->path());
+
+    FileStore::CollectionCreateJob *job = 0;
+
+    // test creating first level collections
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    job = mStore->createCollection(collection1, mStore->topLevelCollection());
+    QVERIFY(job != 0);
+
+    QVERIFY(!job->exec());   // fails, there is an MBox with that name
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+
+    collection1 = job->collection();
+    QVERIFY(collection1.remoteId().isEmpty());
+
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    Collection collection2;
+    collection2.setName(QStringLiteral("collection2"));
+    job = mStore->createCollection(collection2, mStore->topLevelCollection());
+    QVERIFY(job != 0);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection2 = job->collection();
+    QVERIFY(!collection2.remoteId().isEmpty());
+    QVERIFY(collection2.parentCollection() == mStore->topLevelCollection());
+
+    // mbox does not show up as a maildir subfolder
+    QCOMPARE(topLevelMd.subFolderList(), QStringList() << QStringLiteral("collection2"));
+    KPIM::Maildir md2 = topLevelMd.subFolder(collection2.remoteId());
+    QVERIFY(md2.isValid());
+
+    // test creating second level collections inside mbox
+    Collection collection1_1;
+    collection1_1.setName(QStringLiteral("collection1_1"));
+    job = mStore->createCollection(collection1_1, collection1);
+    QVERIFY(job != 0);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection1_1 = job->collection();
+    QVERIFY(!collection1_1.remoteId().isEmpty());
+    QCOMPARE(collection1_1.parentCollection().remoteId(), QStringLiteral("collection1"));
+
+    // treat the MBox subdir path like a top level maildir
+    KPIM::Maildir md1(KPIM::Maildir::subDirPathForFolderPath(fileInfo1.absoluteFilePath()), true);
+    KPIM::Maildir md1_1 = md1.subFolder(collection1_1.remoteId());
+    QVERIFY(md1_1.isValid());
+
+    QCOMPARE(md1.subFolderList(), QStringList() << QStringLiteral("collection1_1"));
+}
+
+QTEST_MAIN(CollectionCreateTest)
+
+#include "collectioncreatetest.moc"
+
diff --git a/resources/mixedmaildir/autotests/collectiondeletetest.cpp b/resources/mixedmaildir/autotests/collectiondeletetest.cpp
new file mode 100644 (file)
index 0000000..3d474f6
--- /dev/null
@@ -0,0 +1,468 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "mixedmaildirstore.h"
+
+#include "filestore/collectiondeletejob.h"
+
+#include "libmaildir/maildir.h"
+
+#include <QTemporaryDir>
+
+#include <qtest.h>
+#include <QFileInfo>
+#include <QDir>
+
+using namespace Akonadi;
+
+class CollectionDeleteTest : public QObject
+{
+    Q_OBJECT
+
+public:
+    CollectionDeleteTest() : QObject(), mStore(0), mDir(0) {}
+    ~CollectionDeleteTest()
+    {
+        delete mStore;
+        delete mDir;
+    }
+
+private:
+    MixedMaildirStore *mStore;
+    QTemporaryDir *mDir;
+
+private Q_SLOTS:
+    void init();
+    void cleanup();
+    void testNonExisting();
+    void testLeaves();
+    void testSubTrees();
+};
+
+void CollectionDeleteTest::init()
+{
+    mStore = new MixedMaildirStore;
+
+    mDir = new QTemporaryDir;
+    QVERIFY(mDir->isValid());
+    QVERIFY(QDir(mDir->path()).exists());
+}
+
+void CollectionDeleteTest::cleanup()
+{
+    delete mStore;
+    mStore = 0;
+    delete mDir;
+    mDir = 0;
+}
+
+void CollectionDeleteTest::testNonExisting()
+{
+    KPIM::Maildir topLevelMd(mDir->path(), true);
+    QVERIFY(topLevelMd.isValid(false));
+
+    KPIM::Maildir md1(topLevelMd.addSubFolder(QStringLiteral("collection1")), false);
+    KPIM::Maildir md1_2(md1.addSubFolder(QStringLiteral("collection1_2")), false);
+
+    KPIM::Maildir md2(topLevelMd.addSubFolder(QStringLiteral("collection2")), false);
+
+    // simulate mbox
+    QFileInfo fileInfo1(mDir->path(), QStringLiteral("collection3"));
+    QFile file1(fileInfo1.absoluteFilePath());
+    file1.open(QIODevice::WriteOnly);
+    file1.close();
+    QVERIFY(fileInfo1.exists());
+
+    // simulate mbox with empty subtree
+    QFileInfo fileInfo2(mDir->path(), QStringLiteral("collection4"));
+    QFile file2(fileInfo2.absoluteFilePath());
+    file2.open(QIODevice::WriteOnly);
+    file2.close();
+    QVERIFY(fileInfo2.exists());
+
+    QFileInfo subDirInfo2(KPIM::Maildir::subDirPathForFolderPath(fileInfo2.absoluteFilePath()));
+    QDir topDir(mDir->path());
+    QVERIFY(topDir.mkpath(subDirInfo2.absoluteFilePath()));
+
+    mStore->setPath(mDir->path());
+
+    FileStore::CollectionDeleteJob *job = 0;
+
+    // test fail of deleting first level collection
+    Collection collection5;
+    collection5.setName(QStringLiteral("collection5"));
+    collection5.setRemoteId(QStringLiteral("collection5"));
+    collection5.setParentCollection(mStore->topLevelCollection());
+    job = mStore->deleteCollection(collection5);
+    QVERIFY(job != 0);
+
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+
+    QCOMPARE(topLevelMd.subFolderList(), QStringList() << QStringLiteral("collection1") << QStringLiteral("collection2"));
+    QVERIFY(fileInfo1.exists());
+
+    // test fail of deleting second level collection in maildir leaf parent
+    Collection collection2;
+    collection2.setName(QStringLiteral("collection2"));
+    collection2.setRemoteId(QStringLiteral("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+
+    Collection collection2_1;
+    collection2_1.setName(QStringLiteral("collection2_1"));
+    collection2_1.setRemoteId(QStringLiteral("collection2_1"));
+    collection2_1.setParentCollection(collection2);
+    job = mStore->deleteCollection(collection2_1);
+    QVERIFY(job != 0);
+
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+
+    QCOMPARE(topLevelMd.subFolderList(), QStringList() << QStringLiteral("collection1") << QStringLiteral("collection2"));
+
+    // test fail of deleting second level collection in maildir parent with subtree
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    Collection collection1_1;
+    collection1_1.setName(QStringLiteral("collection1_1"));
+    collection1_1.setRemoteId(QStringLiteral("collection1_1"));
+    collection1_1.setParentCollection(collection1);
+    job = mStore->deleteCollection(collection1_1);
+    QVERIFY(job != 0);
+
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+
+    QCOMPARE(topLevelMd.subFolderList(), QStringList() << QStringLiteral("collection1") << QStringLiteral("collection2"));
+    QCOMPARE(md1.subFolderList(), QStringList() << QStringLiteral("collection1_2"));
+
+    // test fail of deleting second level collection in mbox leaf parent
+    Collection collection3;
+    collection3.setName(QStringLiteral("collection3"));
+    collection3.setRemoteId(QStringLiteral("collection3"));
+    collection3.setParentCollection(mStore->topLevelCollection());
+
+    Collection collection3_1;
+    collection3_1.setName(QStringLiteral("collection3_1"));
+    collection3_1.setRemoteId(QStringLiteral("collection3_1"));
+    collection3_1.setParentCollection(collection3);
+    job = mStore->deleteCollection(collection3_1);
+    QVERIFY(job != 0);
+
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+
+    QVERIFY(fileInfo1.exists());
+
+    // test fail of deleting second level collection in mbox parent with subtree
+    Collection collection4;
+    collection4.setName(QStringLiteral("collection4"));
+    collection4.setRemoteId(QStringLiteral("collection4"));
+    collection4.setParentCollection(mStore->topLevelCollection());
+
+    Collection collection4_1;
+    collection4_1.setName(QStringLiteral("collection4_1"));
+    collection4_1.setRemoteId(QStringLiteral("collection4_1"));
+    collection4_1.setParentCollection(collection4);
+    job = mStore->deleteCollection(collection4_1);
+    QVERIFY(job != 0);
+
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+
+    QVERIFY(fileInfo2.exists());
+    QVERIFY(subDirInfo2.exists());
+
+    // test fail of deleting second level collection with non existent parent
+    Collection collection5_1;
+    collection5_1.setName(QStringLiteral("collection5_1"));
+    collection5_1.setRemoteId(QStringLiteral("collection5_1"));
+    collection5_1.setParentCollection(collection5);
+    job = mStore->deleteCollection(collection5_1);
+    QVERIFY(job != 0);
+
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+
+    QCOMPARE(topLevelMd.subFolderList(), QStringList() << QStringLiteral("collection1") << QStringLiteral("collection2"));
+    QVERIFY(fileInfo1.exists());
+    QCOMPARE(md1.subFolderList(), QStringList() << QStringLiteral("collection1_2"));
+}
+
+void CollectionDeleteTest::testLeaves()
+{
+    KPIM::Maildir topLevelMd(mDir->path(), true);
+    QVERIFY(topLevelMd.isValid(false));
+
+    QDir topDir(mDir->path());
+
+    KPIM::Maildir md1(topLevelMd.addSubFolder(QStringLiteral("collection1")), false);
+    KPIM::Maildir md1_2(md1.addSubFolder(QStringLiteral("collection1_2")), false);
+
+    // simulate second level mbox in maildir parent
+    QFileInfo fileInfo1_1(KPIM::Maildir::subDirPathForFolderPath(md1.path()),
+                          QStringLiteral("collection1_1"));
+    QFile file1_1(fileInfo1_1.absoluteFilePath());
+    file1_1.open(QIODevice::WriteOnly);
+    file1_1.close();
+    QVERIFY(fileInfo1_1.exists());
+
+    KPIM::Maildir md2(topLevelMd.addSubFolder(QStringLiteral("collection2")), false);
+
+    // simulate first level mbox
+    QFileInfo fileInfo3(mDir->path(), QStringLiteral("collection3"));
+    QFile file3(fileInfo3.absoluteFilePath());
+    file3.open(QIODevice::WriteOnly);
+    file3.close();
+    QVERIFY(fileInfo3.exists());
+
+    // simulate first level mbox with subtree
+    QFileInfo fileInfo4(mDir->path(), QStringLiteral("collection4"));
+    QFile file4(fileInfo4.absoluteFilePath());
+    file4.open(QIODevice::WriteOnly);
+    file4.close();
+    QVERIFY(fileInfo4.exists());
+
+    QFileInfo subDirInfo4(KPIM::Maildir::subDirPathForFolderPath(fileInfo4.absoluteFilePath()));
+    QVERIFY(topDir.mkpath(subDirInfo4.absoluteFilePath()));
+
+    KPIM::Maildir md4(subDirInfo4.absoluteFilePath(), true);
+    KPIM::Maildir md4_1(md4.addSubFolder(QStringLiteral("collection4_1")), false);
+
+    // simulate second level mbox in mbox parent
+    QFileInfo fileInfo4_2(subDirInfo4.absoluteFilePath(),
+                          QStringLiteral("collection4_2"));
+    QFile file4_2(fileInfo4_2.absoluteFilePath());
+    file4_2.open(QIODevice::WriteOnly);
+    file4_2.close();
+    QVERIFY(fileInfo4_2.exists());
+
+    mStore->setPath(mDir->path());
+
+    FileStore::CollectionDeleteJob *job = 0;
+
+    // test second level leaves in maildir parent
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    Collection collection1_1;
+    collection1_1.setName(QStringLiteral("collection1_1"));
+    collection1_1.setRemoteId(QStringLiteral("collection1_1"));
+    collection1_1.setParentCollection(collection1);
+    job = mStore->deleteCollection(collection1_1);
+    QVERIFY(job != 0);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    fileInfo1_1.refresh();
+    QVERIFY(!fileInfo1_1.exists());
+
+    Collection collection1_2;
+    collection1_2.setName(QStringLiteral("collection1_2"));
+    collection1_2.setRemoteId(QStringLiteral("collection1_2"));
+    collection1_2.setParentCollection(collection1);
+    job = mStore->deleteCollection(collection1_2);
+    QVERIFY(job != 0);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    QVERIFY(!md1_2.isValid(false));
+    QCOMPARE(md1.subFolderList(), QStringList());
+
+    // test second level leaves in mbox parent
+    Collection collection4;
+    collection4.setName(QStringLiteral("collection4"));
+    collection4.setRemoteId(QStringLiteral("collection4"));
+    collection4.setParentCollection(mStore->topLevelCollection());
+
+    Collection collection4_1;
+    collection4_1.setName(QStringLiteral("collection4_1"));
+    collection4_1.setRemoteId(QStringLiteral("collection4_1"));
+    collection4_1.setParentCollection(collection4);
+    job = mStore->deleteCollection(collection4_1);
+    QVERIFY(job != 0);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    QVERIFY(!md4_1.isValid(false));
+    QCOMPARE(md4.subFolderList(), QStringList());
+
+    Collection collection4_2;
+    collection4_2.setName(QStringLiteral("collection4_2"));
+    collection4_2.setRemoteId(QStringLiteral("collection4_2"));
+    collection4_2.setParentCollection(collection4);
+    job = mStore->deleteCollection(collection4_2);
+    QVERIFY(job != 0);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    fileInfo4_2.refresh();
+    QVERIFY(!fileInfo4_2.exists());
+
+    // test deleting of first level leaves
+    Collection collection2;
+    collection2.setName(QStringLiteral("collection2"));
+    collection2.setRemoteId(QStringLiteral("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->deleteCollection(collection2);
+    QVERIFY(job != 0);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    QVERIFY(!md2.isValid(false));
+    QCOMPARE(topLevelMd.subFolderList(), QStringList() << QStringLiteral("collection1"));
+
+    Collection collection3;
+    collection3.setName(QStringLiteral("collection3"));
+    collection3.setRemoteId(QStringLiteral("collection3"));
+    collection3.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->deleteCollection(collection3);
+    QVERIFY(job != 0);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    fileInfo3.refresh();
+    QVERIFY(!fileInfo3.exists());
+
+    // test deleting of first level leaves with empty subtrees
+    QFileInfo subDirInfo1(KPIM::Maildir::subDirPathForFolderPath(md1.path()));
+    QVERIFY(subDirInfo1.exists());
+
+    job = mStore->deleteCollection(collection1);
+    QVERIFY(job != 0);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    QVERIFY(!md1.isValid(false));
+    subDirInfo1.refresh();
+    QVERIFY(!subDirInfo1.exists());
+    QCOMPARE(topLevelMd.subFolderList(), QStringList());
+
+    job = mStore->deleteCollection(collection4);
+    QVERIFY(job != 0);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    fileInfo4.refresh();
+    QVERIFY(!fileInfo4.exists());
+    subDirInfo4.refresh();
+    QVERIFY(!subDirInfo4.exists());
+}
+
+void CollectionDeleteTest::testSubTrees()
+{
+    KPIM::Maildir topLevelMd(mDir->path(), true);
+    QVERIFY(topLevelMd.isValid(false));
+
+    QDir topDir(mDir->path());
+
+    KPIM::Maildir md1(topLevelMd.addSubFolder(QStringLiteral("collection1")), false);
+    KPIM::Maildir md1_2(md1.addSubFolder(QStringLiteral("collection1_2")), false);
+
+    // simulate second level mbox in maildir parent
+    QFileInfo fileInfo1_1(KPIM::Maildir::subDirPathForFolderPath(md1.path()),
+                          QStringLiteral("collection1_1"));
+    QFile file1_1(fileInfo1_1.absoluteFilePath());
+    file1_1.open(QIODevice::WriteOnly);
+    file1_1.close();
+    QVERIFY(fileInfo1_1.exists());
+
+    // simulate first level mbox with subtree
+    QFileInfo fileInfo2(mDir->path(), QStringLiteral("collection2"));
+    QFile file2(fileInfo2.absoluteFilePath());
+    file2.open(QIODevice::WriteOnly);
+    file2.close();
+    QVERIFY(fileInfo2.exists());
+
+    QFileInfo subDirInfo2(KPIM::Maildir::subDirPathForFolderPath(fileInfo2.absoluteFilePath()));
+    QVERIFY(topDir.mkpath(subDirInfo2.absoluteFilePath()));
+
+    KPIM::Maildir md2(subDirInfo2.absoluteFilePath(), true);
+    KPIM::Maildir md2_1(md2.addSubFolder(QStringLiteral("collection2_1")), false);
+
+    // simulate second level mbox in mbox parent
+    QFileInfo fileInfo2_2(subDirInfo2.absoluteFilePath(),
+                          QStringLiteral("collection2_2"));
+    QFile file2_2(fileInfo2_2.absoluteFilePath());
+    file2_2.open(QIODevice::WriteOnly);
+    file2_2.close();
+    QVERIFY(fileInfo2_2.exists());
+
+    mStore->setPath(mDir->path());
+
+    FileStore::CollectionDeleteJob *job = 0;
+
+    // test deleting maildir subtree
+    QFileInfo subDirInfo1(KPIM::Maildir::subDirPathForFolderPath(md1.path()));
+    QVERIFY(subDirInfo1.exists());
+
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->deleteCollection(collection1);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    QVERIFY(!md1.isValid(false));
+    QVERIFY(!md1_2.isValid(false));
+    fileInfo1_1.refresh();
+    QVERIFY(!fileInfo1_1.exists());
+
+    // test deleting mbox subtree
+    Collection collection2;
+    collection2.setName(QStringLiteral("collection2"));
+    collection2.setRemoteId(QStringLiteral("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->deleteCollection(collection2);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    fileInfo2.refresh();
+    QVERIFY(!fileInfo2.exists());
+    QVERIFY(!md2_1.isValid(false));
+    fileInfo2_2.refresh();
+    QVERIFY(!fileInfo2_2.exists());
+    QVERIFY(!subDirInfo2.exists());
+}
+
+QTEST_MAIN(CollectionDeleteTest)
+
+#include "collectiondeletetest.moc"
+
diff --git a/resources/mixedmaildir/autotests/collectionfetchtest.cpp b/resources/mixedmaildir/autotests/collectionfetchtest.cpp
new file mode 100644 (file)
index 0000000..938f3ec
--- /dev/null
@@ -0,0 +1,476 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "mixedmaildirstore.h"
+
+#include "filestore/collectionfetchjob.h"
+
+#include "libmaildir/maildir.h"
+
+#include <kmime/kmime_message.h>
+
+#include <QTemporaryDir>
+
+#include <QSignalSpy>
+
+#include <qtest.h>
+#include <QFileInfo>
+#include <QDir>
+
+using namespace Akonadi;
+
+static Collection::List collectionsFromSpy(QSignalSpy *spy)
+{
+    Collection::List collections;
+
+    QListIterator<QList<QVariant> > it(*spy);
+    while (it.hasNext()) {
+        const QList<QVariant> invocation = it.next();
+        Q_ASSERT(invocation.count() == 1);
+
+        collections << invocation.first().value<Collection::List>();
+    }
+
+    return collections;
+}
+
+class CollectionFetchTest : public QObject
+{
+    Q_OBJECT
+
+public:
+    CollectionFetchTest() : QObject(), mStore(0), mDir(0)
+    {
+        // for monitoring signals
+        qRegisterMetaType<Akonadi::Collection::List>();
+    }
+
+    ~CollectionFetchTest()
+    {
+        delete mStore;
+        delete mDir;
+    }
+
+private:
+    MixedMaildirStore *mStore;
+    QTemporaryDir *mDir;
+
+private Q_SLOTS:
+    void init();
+    void cleanup();
+    void testEmptyDir();
+    void testMixedTree();
+};
+
+void CollectionFetchTest::init()
+{
+    mStore = new MixedMaildirStore;
+
+    mDir = new QTemporaryDir;
+    QVERIFY(mDir->isValid());
+    QVERIFY(QDir(mDir->path()).exists());
+}
+
+void CollectionFetchTest::cleanup()
+{
+    delete mStore;
+    mStore = 0;
+    delete mDir;
+    mDir = 0;
+}
+
+void CollectionFetchTest::testEmptyDir()
+{
+    mStore->setPath(mDir->path());
+
+    FileStore::CollectionFetchJob *job = 0;
+    QSignalSpy *spy = 0;
+    Collection::List collections;
+
+    // test base fetch of top level collection
+    job = mStore->fetchCollections(mStore->topLevelCollection(), FileStore::CollectionFetchJob::Base);
+
+    spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+    QCOMPARE(spy->count(), 1);
+
+    collections = collectionsFromSpy(spy);
+    QCOMPARE(collections.count(), 1);
+    QCOMPARE(collections.first(), mStore->topLevelCollection());
+    QCOMPARE(job->collections(), collections);
+
+    // test first level fetch of top level collection
+    job = mStore->fetchCollections(mStore->topLevelCollection(), FileStore::CollectionFetchJob::FirstLevel);
+
+    spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+    QCOMPARE(spy->count(), 0);
+
+    collections = collectionsFromSpy(spy);
+    QCOMPARE(collections.count(), 0);
+    QCOMPARE(job->collections(), collections);
+
+    // test recursive fetch of top level collection
+    job = mStore->fetchCollections(mStore->topLevelCollection(), FileStore::CollectionFetchJob::Recursive);
+
+    spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+    QCOMPARE(spy->count(), 0);
+
+    collections = collectionsFromSpy(spy);
+    QCOMPARE(collections.count(), 0);
+    QCOMPARE(job->collections(), collections);
+
+    // test fail of base fetching non existent collection
+    Collection collection;
+    collection.setName(QStringLiteral("collection"));
+    collection.setRemoteId(QStringLiteral("collection"));
+    collection.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->fetchCollections(collection, FileStore::CollectionFetchJob::Base);
+
+    spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)));
+
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+    QCOMPARE(spy->count(), 0);
+
+    collections = collectionsFromSpy(spy);
+    QCOMPARE(collections.count(), 0);
+    QCOMPARE(job->collections(), collections);
+
+    // test fail of first level fetching non existent collection
+    job = mStore->fetchCollections(collection, FileStore::CollectionFetchJob::FirstLevel);
+
+    spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)));
+
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+    QCOMPARE(spy->count(), 0);
+
+    collections = collectionsFromSpy(spy);
+    QCOMPARE(collections.count(), 0);
+    QCOMPARE(job->collections(), collections);
+
+    // test fail of recursive fetching non existent collection
+    job = mStore->fetchCollections(collection, FileStore::CollectionFetchJob::FirstLevel);
+
+    spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)));
+
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+    QCOMPARE(spy->count(), 0);
+
+    collections = collectionsFromSpy(spy);
+    QCOMPARE(collections.count(), 0);
+    QCOMPARE(job->collections(), collections);
+}
+
+void CollectionFetchTest::testMixedTree()
+{
+    QDir topDir(mDir->path());
+
+    KPIM::Maildir topLevelMd(mDir->path(), true);
+    QVERIFY(topLevelMd.isValid());
+
+    KPIM::Maildir md1(topLevelMd.addSubFolder(QStringLiteral("collection1")), false);
+    KPIM::Maildir md1_2(md1.addSubFolder(QStringLiteral("collection1_2")), false);
+    KPIM::Maildir md1_2_1(md1_2.addSubFolder(QStringLiteral("collection1_2_1")), false);
+
+    // simulate second level mbox in maildir parent
+    QFileInfo fileInfo1_1(KPIM::Maildir::subDirPathForFolderPath(md1.path()),
+                          QStringLiteral("collection1_1"));
+    QFile file1_1(fileInfo1_1.absoluteFilePath());
+    file1_1.open(QIODevice::WriteOnly);
+    file1_1.close();
+    QVERIFY(fileInfo1_1.exists());
+
+    QFileInfo subDirInfo1_1(KPIM::Maildir::subDirPathForFolderPath(fileInfo1_1.absoluteFilePath()));
+    QVERIFY(topDir.mkpath(subDirInfo1_1.absoluteFilePath()));
+    KPIM::Maildir md1_1(subDirInfo1_1.absoluteFilePath(), true);
+    KPIM::Maildir md1_1_1(md1_1.addSubFolder(QStringLiteral("collection1_1_1")), false);
+
+    // simulate third level mbox in mbox parent
+    QFileInfo fileInfo1_1_2(md1_1.path(), QStringLiteral("collection1_1_2"));
+    QFile file1_1_2(fileInfo1_1_2.absoluteFilePath());
+    file1_1_2.open(QIODevice::WriteOnly);
+    file1_1_2.close();
+    QVERIFY(fileInfo1_1_2.exists());
+
+    KPIM::Maildir md2(topLevelMd.addSubFolder(QStringLiteral("collection2")), false);
+
+    // simulate first level mbox
+    QFileInfo fileInfo3(mDir->path(), QStringLiteral("collection3"));
+    QFile file3(fileInfo3.absoluteFilePath());
+    file3.open(QIODevice::WriteOnly);
+    file3.close();
+    QVERIFY(fileInfo3.exists());
+
+    // simulate first level mbox with subtree
+    QFileInfo fileInfo4(mDir->path(), QStringLiteral("collection4"));
+    QFile file4(fileInfo4.absoluteFilePath());
+    file4.open(QIODevice::WriteOnly);
+    file4.close();
+    QVERIFY(fileInfo4.exists());
+
+    QFileInfo subDirInfo4(KPIM::Maildir::subDirPathForFolderPath(fileInfo4.absoluteFilePath()));
+    QVERIFY(topDir.mkpath(subDirInfo4.absoluteFilePath()));
+
+    KPIM::Maildir md4(subDirInfo4.absoluteFilePath(), true);
+    KPIM::Maildir md4_1(md4.addSubFolder(QStringLiteral("collection4_1")), false);
+
+    // simulate second level mbox in mbox parent
+    QFileInfo fileInfo4_2(subDirInfo4.absoluteFilePath(),
+                          QStringLiteral("collection4_2"));
+    QFile file4_2(fileInfo4_2.absoluteFilePath());
+    file4_2.open(QIODevice::WriteOnly);
+    file4_2.close();
+    QVERIFY(fileInfo4_2.exists());
+
+    QSet<QString> firstLevelNames;
+    firstLevelNames << md1.name() << md2.name() << fileInfo3.fileName() << fileInfo4.fileName();
+
+    QSet<QString> secondLevelNames;
+    secondLevelNames << md1_2.name() << md4_1.name()
+                     << fileInfo1_1.fileName() << fileInfo4_2.fileName();
+
+    QSet<QString> thirdLevelNames;
+    thirdLevelNames << md1_1_1.name() << fileInfo1_1_2.fileName() << md1_2_1.name();
+
+    mStore->setPath(mDir->path());
+    //mDir = 0;
+
+    FileStore::CollectionFetchJob *job = 0;
+    QSignalSpy *spy = 0;
+    Collection::List collections;
+
+    // test base fetch of top level collection
+    job = mStore->fetchCollections(mStore->topLevelCollection(), FileStore::CollectionFetchJob::Base);
+
+    spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+    QCOMPARE(spy->count(), 1);
+
+    collections = collectionsFromSpy(spy);
+    QCOMPARE(collections.count(), 1);
+    QCOMPARE(collections.first(), mStore->topLevelCollection());
+    QCOMPARE(job->collections(), collections);
+
+    // test first level fetch of top level collection
+    job = mStore->fetchCollections(mStore->topLevelCollection(), FileStore::CollectionFetchJob::FirstLevel);
+
+    spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+    QVERIFY(spy->count() > 0);
+
+    collections = collectionsFromSpy(spy);
+    QCOMPARE(collections.count(), firstLevelNames.count());
+    QCOMPARE(job->collections(), collections);
+
+    Q_FOREACH (const Collection &collection, collections) {
+        QVERIFY(!collection.remoteId().isEmpty());
+        QCOMPARE(collection.remoteId(), collection.name());
+        QCOMPARE(collection.contentMimeTypes(), QStringList() << Collection::mimeType() << KMime::Message::mimeType());
+
+        QCOMPARE(collection.rights(), Collection::CanCreateItem |
+                 Collection::CanChangeItem |
+                 Collection::CanDeleteItem |
+                 Collection::CanCreateCollection |
+                 Collection::CanChangeCollection |
+                 Collection::CanDeleteCollection);
+
+        QCOMPARE(collection.parentCollection(), mStore->topLevelCollection());
+        QVERIFY(firstLevelNames.contains(collection.name()));
+    }
+
+    // test recursive fetch of top level collection
+    job = mStore->fetchCollections(mStore->topLevelCollection(), FileStore::CollectionFetchJob::Recursive);
+
+    spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+    QVERIFY(spy->count() > 0);
+
+    collections = collectionsFromSpy(spy);
+    QCOMPARE(collections.count(),
+             firstLevelNames.count() + secondLevelNames.count() + thirdLevelNames.count());
+    QCOMPARE(job->collections(), collections);
+
+    Q_FOREACH (const Collection &collection, collections) {
+        QVERIFY(!collection.remoteId().isEmpty());
+        QCOMPARE(collection.remoteId(), collection.name());
+        QCOMPARE(collection.contentMimeTypes(), QStringList() << Collection::mimeType() << KMime::Message::mimeType());
+
+        QCOMPARE(collection.rights(), Collection::CanCreateItem |
+                 Collection::CanChangeItem |
+                 Collection::CanDeleteItem |
+                 Collection::CanCreateCollection |
+                 Collection::CanChangeCollection |
+                 Collection::CanDeleteCollection);
+
+        if (firstLevelNames.contains(collection.name())) {
+            QCOMPARE(collection.parentCollection(), mStore->topLevelCollection());
+        } else if (secondLevelNames.contains(collection.name())) {
+            QVERIFY(firstLevelNames.contains(collection.parentCollection().name()));
+            QCOMPARE(collection.parentCollection().parentCollection(), mStore->topLevelCollection());
+        } else if (thirdLevelNames.contains(collection.name())) {
+            QVERIFY(secondLevelNames.contains(collection.parentCollection().name()));
+            QCOMPARE(collection.parentCollection().parentCollection().parentCollection(),
+                     mStore->topLevelCollection());
+        }
+    }
+
+    // test base fetching all collections
+    Q_FOREACH (const Collection &collection, collections) {
+        job = mStore->fetchCollections(collection, FileStore::CollectionFetchJob::Base);
+
+        spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)));
+
+        QVERIFY(job->exec());
+        QCOMPARE(job->error(), 0);
+        QCOMPARE(spy->count(), 1);
+
+        const Collection::List list = collectionsFromSpy(spy);
+        QCOMPARE(list.count(), 1);
+        QCOMPARE(list.first(), collection);
+        QCOMPARE(job->collections(), list);
+
+        const Collection col = list.first();
+        QVERIFY(!col.remoteId().isEmpty());
+        QCOMPARE(col.remoteId(), col.name());
+        QCOMPARE(col.contentMimeTypes(), QStringList() << Collection::mimeType() << KMime::Message::mimeType());
+
+        QCOMPARE(col.rights(), Collection::CanCreateItem |
+                 Collection::CanChangeItem |
+                 Collection::CanDeleteItem |
+                 Collection::CanCreateCollection |
+                 Collection::CanChangeCollection |
+                 Collection::CanDeleteCollection);
+    }
+
+    // test first level fetching all collections
+    Q_FOREACH (const Collection &collection, collections) {
+        job = mStore->fetchCollections(collection, FileStore::CollectionFetchJob::FirstLevel);
+
+        spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)));
+
+        QVERIFY(job->exec());
+        QCOMPARE(job->error(), 0);
+
+        const Collection::List list = collectionsFromSpy(spy);
+        QCOMPARE(job->collections(), list);
+
+        Q_FOREACH (const Collection &childCollection, list) {
+            QCOMPARE(childCollection.parentCollection(), collection);
+
+            QVERIFY(!childCollection.remoteId().isEmpty());
+            QCOMPARE(childCollection.remoteId(), childCollection.name());
+            QCOMPARE(childCollection.contentMimeTypes(), QStringList() << Collection::mimeType() << KMime::Message::mimeType());
+
+            QCOMPARE(childCollection.rights(), Collection::CanCreateItem |
+                     Collection::CanChangeItem |
+                     Collection::CanDeleteItem |
+                     Collection::CanCreateCollection |
+                     Collection::CanChangeCollection |
+                     Collection::CanDeleteCollection);
+        }
+
+        if (firstLevelNames.contains(collection.name())) {
+            Q_FOREACH (const Collection &childCollection, list) {
+                QVERIFY(secondLevelNames.contains(childCollection.name()));
+            }
+        } else if (secondLevelNames.contains(collection.name())) {
+            Q_FOREACH (const Collection &childCollection, list) {
+                QVERIFY(thirdLevelNames.contains(childCollection.name()));
+            }
+            if (collection.name() == md1_2.name()) {
+                QCOMPARE(list.count(), 1);
+                QCOMPARE(list.first().name(), md1_2_1.name());
+            } else if (collection.name() == fileInfo1_1.fileName()) {
+                QCOMPARE(list.count(), 2);
+            }
+        } else {
+            QCOMPARE(list.count(), 0);
+        }
+    }
+
+    // test recursive fetching all collections
+    Q_FOREACH (const Collection &collection, collections) {
+        job = mStore->fetchCollections(collection, FileStore::CollectionFetchJob::Recursive);
+
+        spy = new QSignalSpy(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)));
+
+        QVERIFY(job->exec());
+        QCOMPARE(job->error(), 0);
+
+        const Collection::List list = collectionsFromSpy(spy);
+        QCOMPARE(job->collections(), list);
+
+        Q_FOREACH (const Collection &childCollection, list) {
+            QVERIFY(childCollection.parentCollection() == collection ||
+                    childCollection.parentCollection().parentCollection() == collection);
+            QVERIFY(!childCollection.remoteId().isEmpty());
+            QCOMPARE(childCollection.remoteId(), childCollection.name());
+            QCOMPARE(childCollection.contentMimeTypes(), QStringList() << Collection::mimeType() << KMime::Message::mimeType());
+
+            QCOMPARE(childCollection.rights(), Collection::CanCreateItem |
+                     Collection::CanChangeItem |
+                     Collection::CanDeleteItem |
+                     Collection::CanCreateCollection |
+                     Collection::CanChangeCollection |
+                     Collection::CanDeleteCollection);
+        }
+
+        if (firstLevelNames.contains(collection.name())) {
+            Q_FOREACH (const Collection &childCollection, list) {
+                QVERIFY(secondLevelNames.contains(childCollection.name()) ||
+                        thirdLevelNames.contains(childCollection.name()));
+            }
+        } else if (secondLevelNames.contains(collection.name())) {
+            Q_FOREACH (const Collection &childCollection, list) {
+                QVERIFY(thirdLevelNames.contains(childCollection.name()));
+            }
+            if (collection.name() == md1_2.name()) {
+                QCOMPARE(list.count(), 1);
+                QCOMPARE(list.first().name(), md1_2_1.name());
+            } else if (collection.name() == fileInfo1_1.fileName()) {
+                QCOMPARE(list.count(), 2);
+            }
+        } else {
+            QCOMPARE(list.count(), 0);
+        }
+    }
+}
+
+QTEST_MAIN(CollectionFetchTest)
+
+#include "collectionfetchtest.moc"
+
diff --git a/resources/mixedmaildir/autotests/collectionmodifytest.cpp b/resources/mixedmaildir/autotests/collectionmodifytest.cpp
new file mode 100644 (file)
index 0000000..f99c384
--- /dev/null
@@ -0,0 +1,631 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "mixedmaildirstore.h"
+
+#include "testdatautil.h"
+
+#include "filestore/collectionmodifyjob.h"
+#include "filestore/itemfetchjob.h"
+
+#include "libmaildir/maildir.h"
+
+#include <QTemporaryDir>
+
+#include <qtest.h>
+#include <QFileInfo>
+#include <QDir>
+
+using namespace Akonadi;
+
+class CollectionModifyTest : public QObject
+{
+    Q_OBJECT
+
+public:
+    CollectionModifyTest() : QObject(), mStore(0), mDir(0) {}
+
+    ~CollectionModifyTest()
+    {
+        delete mStore;
+        delete mDir;
+    }
+
+private:
+    MixedMaildirStore *mStore;
+    QTemporaryDir *mDir;
+
+private Q_SLOTS:
+    void init();
+    void cleanup();
+    void testRename();
+    void testIndexPreservation();
+    void testIndexCacheUpdate();
+};
+
+void CollectionModifyTest::init()
+{
+    mStore = new MixedMaildirStore;
+
+    mDir = new QTemporaryDir;
+    QVERIFY(mDir->isValid());
+    QVERIFY(QDir(mDir->path()).exists());
+}
+
+void CollectionModifyTest::cleanup()
+{
+    delete mStore;
+    mStore = 0;
+    delete mDir;
+    mDir = 0;
+}
+
+void CollectionModifyTest::testRename()
+{
+    QDir topDir(mDir->path());
+    QVERIFY(topDir.mkdir(QStringLiteral("topLevel")));
+    QVERIFY(topDir.cd(QStringLiteral("topLevel")));
+
+    KPIM::Maildir topLevelMd(topDir.path(), true);
+    QVERIFY(topLevelMd.isValid(false));
+
+    KPIM::Maildir md1(topLevelMd.addSubFolder(QStringLiteral("collection1")), false);
+    KPIM::Maildir md1_2(md1.addSubFolder(QStringLiteral("collection1_2")), false);
+
+    // simulate second level mbox in maildir parent
+    QFileInfo fileInfo1_1(KPIM::Maildir::subDirPathForFolderPath(md1.path()),
+                          QStringLiteral("collection1_1"));
+    QFile file1_1(fileInfo1_1.absoluteFilePath());
+    file1_1.open(QIODevice::WriteOnly);
+    file1_1.close();
+    QVERIFY(fileInfo1_1.exists());
+
+    KPIM::Maildir md2(topLevelMd.addSubFolder(QStringLiteral("collection2")), false);
+
+    // simulate first level mbox
+    QFileInfo fileInfo3(topDir.path(), QStringLiteral("collection3"));
+    QFile file3(fileInfo3.absoluteFilePath());
+    file3.open(QIODevice::WriteOnly);
+    file3.close();
+    QVERIFY(fileInfo3.exists());
+
+    // simulate first level mbox with subtree
+    QFileInfo fileInfo4(topDir.path(), QStringLiteral("collection4"));
+    QFile file4(fileInfo4.absoluteFilePath());
+    file4.open(QIODevice::WriteOnly);
+    file4.close();
+    QVERIFY(fileInfo4.exists());
+
+    QFileInfo subDirInfo4(KPIM::Maildir::subDirPathForFolderPath(fileInfo4.absoluteFilePath()));
+    QVERIFY(topDir.mkpath(subDirInfo4.absoluteFilePath()));
+
+    KPIM::Maildir md4(subDirInfo4.absoluteFilePath(), true);
+    KPIM::Maildir md4_1(md4.addSubFolder(QStringLiteral("collection4_1")), false);
+
+    // simulate second level mbox in mbox parent
+    QFileInfo fileInfo4_2(subDirInfo4.absoluteFilePath(),
+                          QStringLiteral("collection4_2"));
+    QFile file4_2(fileInfo4_2.absoluteFilePath());
+    file4_2.open(QIODevice::WriteOnly);
+    file4_2.close();
+    QVERIFY(fileInfo4_2.exists());
+
+    mStore->setPath(topDir.path());
+
+    FileStore::CollectionModifyJob *job = 0;
+    Collection collection;
+
+    // test renaming top level collection
+    topDir.cdUp();
+    QVERIFY(!topDir.exists(QStringLiteral("newTopLevel")));
+
+    Collection topLevelCollection = mStore->topLevelCollection();
+    topLevelCollection.setName(QStringLiteral("newTopLevel"));
+    job = mStore->modifyCollection(topLevelCollection);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    QVERIFY(topDir.exists(QStringLiteral("newTopLevel")));
+    QVERIFY(!topDir.exists(QStringLiteral("topLevel")));
+    QVERIFY(topDir.cd(QStringLiteral("newTopLevel")));
+    QCOMPARE(mStore->path(), topDir.path());
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), mStore->path());
+    QCOMPARE(collection, mStore->topLevelCollection());
+
+    // test failure of renaming again
+    job = mStore->modifyCollection(topLevelCollection);
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int) FileStore::Job::InvalidJobContext);
+    QCOMPARE(collection.remoteId(), mStore->path());
+    QCOMPARE(collection, mStore->topLevelCollection());
+
+    // adjust local handles
+    topLevelMd = KPIM::Maildir(topDir.path(), true);
+    QVERIFY(topLevelMd.isValid(false));
+
+    md1 = topLevelMd.subFolder(QStringLiteral("collection1"));
+    QVERIFY(md1.isValid(false));
+    md1_2 = md1.subFolder(QStringLiteral("collection1_2"));
+
+    fileInfo1_1 = QFileInfo(KPIM::Maildir::subDirPathForFolderPath(md1.path()),
+                            QStringLiteral("collection1_1"));
+    QVERIFY(fileInfo1_1.exists());
+
+    md2 = topLevelMd.subFolder(QStringLiteral("collection2"));
+
+    fileInfo3 = QFileInfo(topDir.path(), QStringLiteral("collection3"));
+    QVERIFY(fileInfo3.exists());
+
+    fileInfo4 = QFileInfo(topDir.path(), QStringLiteral("collection4"));
+    QVERIFY(fileInfo4.exists());
+
+    subDirInfo4 = QFileInfo(KPIM::Maildir::subDirPathForFolderPath(fileInfo4.absoluteFilePath()));
+    QVERIFY(subDirInfo4.exists());
+
+    md4 = KPIM::Maildir(subDirInfo4.absoluteFilePath(), true);
+    QVERIFY(md4.isValid(false));
+    md4_1 = md4.subFolder(QStringLiteral("collection4_1"));
+
+    fileInfo4_2 = QFileInfo(subDirInfo4.absoluteFilePath(),
+                            QStringLiteral("collection4_2"));
+    QVERIFY(fileInfo4_2.exists());
+
+    QCOMPARE(topLevelMd.subFolderList(), QStringList() << QStringLiteral("collection1") << QStringLiteral("collection2"));
+
+    // test rename first level maildir leaf
+    Collection collection2;
+    collection2.setRemoteId(QStringLiteral("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+    collection2.setName(QStringLiteral("collection2_renamed"));
+
+    job = mStore->modifyCollection(collection2);
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection.name());
+    QCOMPARE(collection, collection2);
+    QCOMPARE(topLevelMd.subFolderList(), QStringList() << QStringLiteral("collection1") << QStringLiteral("collection2_renamed"));
+    QVERIFY(!md2.isValid(false));
+    md2 = topLevelMd.subFolder(collection.remoteId());
+    QVERIFY(md2.isValid(false));
+
+    // test failure of renaming again
+    job = mStore->modifyCollection(collection2);
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int) FileStore::Job::InvalidJobContext);
+    QCOMPARE(topLevelMd.subFolderList(), QStringList() << QStringLiteral("collection1") << QStringLiteral("collection2_renamed"));
+    QVERIFY(md2.isValid(false));
+
+    // test renaming of first level mbox leaf
+    Collection collection3;
+    collection3.setRemoteId(QStringLiteral("collection3"));
+    collection3.setParentCollection(mStore->topLevelCollection());
+    collection3.setName(QStringLiteral("collection3_renamed"));
+
+    job = mStore->modifyCollection(collection3);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection.name());
+    QCOMPARE(collection, collection3);
+    fileInfo3.refresh();
+    QVERIFY(!fileInfo3.exists());
+    fileInfo3 = QFileInfo(topDir.path(), collection.remoteId());
+    QVERIFY(fileInfo3.exists());
+
+    // test failure of renaming again
+    job = mStore->modifyCollection(collection3);
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int) FileStore::Job::InvalidJobContext);
+    fileInfo3.refresh();
+    QVERIFY(fileInfo3.exists());
+
+    // test renaming second level maildir in mbox parent
+    Collection collection4;
+    collection4.setRemoteId(QStringLiteral("collection4"));
+    collection4.setParentCollection(mStore->topLevelCollection());
+    collection4.setName(QStringLiteral("collection4"));
+
+    Collection collection4_1;
+    collection4_1.setRemoteId(QStringLiteral("collection4_1"));
+    collection4_1.setParentCollection(collection4);
+    collection4_1.setName(QStringLiteral("collection4_1_renamed"));
+
+    job = mStore->modifyCollection(collection4_1);
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection.name());
+    QCOMPARE(collection, collection4_1);
+    QCOMPARE(md4.subFolderList(), QStringList() << QStringLiteral("collection4_1_renamed"));
+    QVERIFY(!md4_1.isValid(false));
+    md4_1 = md4.subFolder(collection.remoteId());
+    QVERIFY(md4_1.isValid(false));
+
+    // test failure of renaming again
+    job = mStore->modifyCollection(collection4_1);
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int) FileStore::Job::InvalidJobContext);
+    QCOMPARE(md4.subFolderList(), QStringList() << QStringLiteral("collection4_1_renamed"));
+    QVERIFY(md4_1.isValid(false));
+
+    // test renaming of second level mbox in mbox parent
+    Collection collection4_2;
+    collection4_2.setRemoteId(QStringLiteral("collection4_2"));
+    collection4_2.setParentCollection(collection4);
+    collection4_2.setName(QStringLiteral("collection4_2_renamed"));
+
+    job = mStore->modifyCollection(collection4_2);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection.name());
+    QCOMPARE(collection, collection4_2);
+    fileInfo4_2.refresh();
+    QVERIFY(!fileInfo4_2.exists());
+    fileInfo4_2 = QFileInfo(md4.path(), collection.remoteId());
+    QVERIFY(fileInfo4_2.exists());
+
+    // test failure of renaming again
+    job = mStore->modifyCollection(collection4_2);
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int) FileStore::Job::InvalidJobContext);
+    fileInfo4_2.refresh();
+    QVERIFY(fileInfo4_2.exists());
+
+    // test renaming of maildir with subtree
+    Collection collection1;
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+    collection1.setName(QStringLiteral("collection1_renamed"));
+
+    job = mStore->modifyCollection(collection1);
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection.name());
+    QCOMPARE(collection, collection1);
+    QCOMPARE(topLevelMd.subFolderList(), QStringList() << QStringLiteral("collection1_renamed") << QStringLiteral("collection2_renamed"));
+    QVERIFY(!md1.isValid(false));
+    md1 = topLevelMd.subFolder(collection.remoteId());
+    QVERIFY(md1.isValid(false));
+    fileInfo1_1.refresh();
+    QVERIFY(!fileInfo1_1.exists());
+    QVERIFY(!md1_2.isValid(false));
+    fileInfo1_1 = QFileInfo(KPIM::Maildir::subDirPathForFolderPath(md1.path()),
+                            QStringLiteral("collection1_1"));
+    QVERIFY(fileInfo1_1.exists());
+    md1_2 = md1.subFolder(QStringLiteral("collection1_2"));
+    QVERIFY(md1_2.isValid(false));
+
+    // test failure of renaming again
+    job = mStore->modifyCollection(collection1);
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int) FileStore::Job::InvalidJobContext);
+    QCOMPARE(topLevelMd.subFolderList(), QStringList() << QStringLiteral("collection1_renamed") << QStringLiteral("collection2_renamed"));
+    QVERIFY(md2.isValid(false));
+    QVERIFY(fileInfo1_1.exists());
+    QVERIFY(md1_2.isValid(false));
+
+    // test renaming of mbox with subtree
+    collection4.setName(QStringLiteral("collection4_renamed"));
+    job = mStore->modifyCollection(collection4);
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection.name());
+    QCOMPARE(collection, collection4);
+    fileInfo4.refresh();
+    QVERIFY(!fileInfo4.exists());
+    fileInfo4 = QFileInfo(topDir.path(), collection.remoteId());
+    QVERIFY(fileInfo4.exists());
+    md4 = KPIM::Maildir(KPIM::Maildir::subDirPathForFolderPath(fileInfo4.absoluteFilePath()), true);
+    QVERIFY(md4.isValid(false));
+
+    QVERIFY(!md4_1.isValid(false));
+    fileInfo4_2.refresh();
+    QVERIFY(!fileInfo4_2.exists());
+    md4_1 = md4.subFolder(QStringLiteral("collection4_1_renamed"));
+    QVERIFY(md4_1.isValid(false));
+    fileInfo4_2 = QFileInfo(md4.path(), QStringLiteral("collection4_2_renamed"));
+    QVERIFY(fileInfo4_2.exists());
+
+    // test failure of renaming again
+    job = mStore->modifyCollection(collection4);
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int) FileStore::Job::InvalidJobContext);
+    fileInfo4.refresh();
+    QVERIFY(fileInfo4.exists());
+}
+
+void CollectionModifyTest::testIndexPreservation()
+{
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), mDir->path(), QStringLiteral("collection1")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), mDir->path(), QStringLiteral("collection2")));
+
+    mStore->setPath(mDir->path());
+
+    const QVariant colListVar = QVariant::fromValue<Collection::List>(Collection::List());
+    FileStore::CollectionModifyJob *job = 0;
+    FileStore::ItemFetchJob *itemFetch = 0;
+    QVariant var;
+    Collection::List collections;
+    Item::List items;
+
+    QMap<QByteArray, int> flagCounts;
+
+    // test renaming mbox
+    Collection collection1;
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+    collection1.setName(QStringLiteral("collection1_renamed"));
+
+    job = mStore->modifyCollection(collection1);
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection1);
+
+    const QFileInfo indexFileInfo1(mDir->path(), QStringLiteral(".collection1_renamed.index"));
+    QVERIFY(!indexFileInfo1.exists());
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collections.first());
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+
+    // test renaming maildir
+    Collection collection2;
+    collection2.setRemoteId(QStringLiteral("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+    collection2.setName(QStringLiteral("collection2_renamed"));
+
+    job = mStore->modifyCollection(collection2);
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection2);
+
+    const QFileInfo indexFileInfo2(mDir->path(), QStringLiteral(".collection2_renamed.index"));
+    QVERIFY(!indexFileInfo2.exists());
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collections.first());
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+
+    flagCounts.clear();
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+}
+
+void CollectionModifyTest::testIndexCacheUpdate()
+{
+    KPIM::Maildir topLevelMd(mDir->path(), true);
+    QVERIFY(topLevelMd.isValid(false));
+
+    KPIM::Maildir md1(topLevelMd.addSubFolder(QStringLiteral("collection1")), false);
+
+    // simulate first level mbox
+    QFileInfo fileInfo2(mDir->path(), QStringLiteral("collection2"));
+    QFile file2(fileInfo2.absoluteFilePath());
+    file2.open(QIODevice::WriteOnly);
+    file2.close();
+    QVERIFY(fileInfo2.exists());
+
+    const QString colSubDir1 = KPIM::Maildir::subDirPathForFolderPath(md1.path());
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), colSubDir1, QStringLiteral("collection1_1")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), colSubDir1, QStringLiteral("collection1_2")));
+
+    const QString colSubDir2 = KPIM::Maildir::subDirPathForFolderPath(fileInfo2.absoluteFilePath());
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), colSubDir2, QStringLiteral("collection2_1")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), colSubDir2, QStringLiteral("collection2_2")));
+
+    mStore->setPath(mDir->path());
+
+    FileStore::CollectionModifyJob *job = 0;
+    FileStore::ItemFetchJob *itemFetch = 0;
+    Collection collection;
+    Item::List items;
+    QMap<QByteArray, int> flagCounts;
+
+    // preparation: load all second level items to make sure respective index data is cached
+    Collection collection1;
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+    collection1.setName(QStringLiteral("collection1"));
+
+    Collection collection1_1;
+    collection1_1.setRemoteId(QStringLiteral("collection1_1"));
+    collection1_1.setParentCollection(collection1);
+    collection1_1.setName(QStringLiteral("collection1_1"));
+
+    itemFetch = mStore->fetchItems(collection1_1);
+    QVERIFY(itemFetch->exec());
+
+    Collection collection1_2;
+    collection1_2.setRemoteId(QStringLiteral("collection1_2"));
+    collection1_2.setParentCollection(collection1);
+    collection1_2.setName(QStringLiteral("collection1_2"));
+
+    itemFetch = mStore->fetchItems(collection1_2);
+    QVERIFY(itemFetch->exec());
+    Collection collection2;
+    collection2.setRemoteId(QStringLiteral("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+    collection2.setName(QStringLiteral("collection2"));
+
+    Collection collection2_1;
+    collection2_1.setRemoteId(QStringLiteral("collection2_1"));
+    collection2_1.setParentCollection(collection2);
+    collection2_1.setName(QStringLiteral("collection2_1"));
+
+    itemFetch = mStore->fetchItems(collection2_1);
+    QVERIFY(itemFetch->exec());
+    Collection collection2_2;
+    collection2_2.setRemoteId(QStringLiteral("collection2_2"));
+    collection2_2.setParentCollection(collection2);
+    collection2_2.setName(QStringLiteral("collection2_2"));
+
+    itemFetch = mStore->fetchItems(collection2_2);
+    QVERIFY(itemFetch->exec());
+
+    // test renaming the maildir parent
+    collection1.setName(QStringLiteral("collection1_renamed"));
+
+    job = mStore->modifyCollection(collection1);
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+
+    // get the items of the children and check the flags (see data/README)
+    collection1_1.setParentCollection(collection);
+    itemFetch = mStore->fetchItems(collection1_1);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    collection1_2.setParentCollection(collection);
+    itemFetch = mStore->fetchItems(collection1_2);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // test renaming the mbox parent
+    collection2.setName(QStringLiteral("collection2_renamed"));
+
+    job = mStore->modifyCollection(collection2);
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+
+    // get the items of the children and check the flags (see data/README)
+    collection2_1.setParentCollection(collection);
+    itemFetch = mStore->fetchItems(collection2_1);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    collection2_2.setParentCollection(collection);
+    itemFetch = mStore->fetchItems(collection2_2);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+}
+
+QTEST_MAIN(CollectionModifyTest)
+
+#include "collectionmodifytest.moc"
+
diff --git a/resources/mixedmaildir/autotests/collectionmovetest.cpp b/resources/mixedmaildir/autotests/collectionmovetest.cpp
new file mode 100644 (file)
index 0000000..abfd51a
--- /dev/null
@@ -0,0 +1,2002 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "mixedmaildirstore.h"
+
+#include "testdatautil.h"
+
+#include "filestore/collectionmovejob.h"
+#include "filestore/itemfetchjob.h"
+
+#include "libmaildir/maildir.h"
+
+#include <QTemporaryDir>
+
+#include <qtest.h>
+#include <QFileInfo>
+#include <QDir>
+
+using namespace Akonadi;
+
+class CollectionMoveTest : public QObject
+{
+    Q_OBJECT
+
+public:
+    CollectionMoveTest() : QObject(), mStore(0), mDir(0) {}
+
+    ~CollectionMoveTest()
+    {
+        delete mStore;
+        delete mDir;
+    }
+
+private:
+    MixedMaildirStore *mStore;
+    QTemporaryDir *mDir;
+
+private Q_SLOTS:
+    void init();
+    void cleanup();
+    void testMoveToTopLevel();
+    void testMoveToMaildir();
+    void testMoveToMBox();
+};
+
+void CollectionMoveTest::init()
+{
+    mStore = new MixedMaildirStore;
+
+    mDir = new QTemporaryDir;
+    QVERIFY(mDir->isValid());
+    QVERIFY(QDir(mDir->path()).exists());
+}
+
+void CollectionMoveTest::cleanup()
+{
+    delete mStore;
+    mStore = 0;
+    delete mDir;
+    mDir = 0;
+}
+
+void CollectionMoveTest::testMoveToTopLevel()
+{
+    QDir topDir(mDir->path());
+
+    // top level dir
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), topDir.path(), QStringLiteral("collection1")));
+    QFileInfo fileInfo1(topDir, QStringLiteral("collection1"));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), topDir.path(), QStringLiteral("collection2")));
+    QFileInfo fileInfo2(topDir, QStringLiteral("collection2"));
+
+    // first level maildir parent
+    QDir subDir1 = topDir;
+    QVERIFY(subDir1.mkdir(QStringLiteral(".collection1.directory")));
+    QVERIFY(subDir1.cd(QStringLiteral(".collection1.directory")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), subDir1.path(), QStringLiteral("collection1_1")));
+    QFileInfo fileInfo1_1(subDir1.path(), QStringLiteral("collection1_1"));
+    QVERIFY(fileInfo1_1.exists());
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), subDir1.path(), QStringLiteral("collection1_2")));
+    QFileInfo fileInfo1_2(subDir1.path(), QStringLiteral("collection1_2"));
+    QVERIFY(fileInfo1_2.exists());
+
+    // first level mbox parent
+    QDir subDir2 = topDir;
+    QVERIFY(subDir2.mkdir(QStringLiteral(".collection2.directory")));
+    QVERIFY(subDir2.cd(QStringLiteral(".collection2.directory")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), subDir2.path(), QStringLiteral("collection2_1")));
+    QFileInfo fileInfo2_1(subDir2.path(), QStringLiteral("collection2_1"));
+    QVERIFY(fileInfo2_1.exists());
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), subDir2.path(), QStringLiteral("collection2_2")));
+    QFileInfo fileInfo2_2(subDir2.path(), QStringLiteral("collection2_2"));
+    QVERIFY(fileInfo2_2.exists());
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    FileStore::CollectionMoveJob *job = 0;
+    FileStore::ItemFetchJob *itemFetch = 0;
+    Collection collection;
+    const QVariant colListVar = QVariant::fromValue<Collection::List>(Collection::List());
+    QVariant var;
+    Collection::List collections;
+    Item::List items;
+    QMap<QByteArray, int> flagCounts;
+
+    // test moving maildir from maildir parent
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    Collection collection1_1;
+    collection1_1.setName(QStringLiteral("collection1_1"));
+    collection1_1.setRemoteId(QStringLiteral("collection1_1"));
+    collection1_1.setParentCollection(collection1);
+
+    job = mStore->moveCollection(collection1_1, mStore->topLevelCollection());
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection1_1.remoteId());
+    QCOMPARE(collection.parentCollection(), mStore->topLevelCollection());
+
+    fileInfo1_1.refresh();
+    QVERIFY(!fileInfo1_1.exists());
+    fileInfo1_1 = QFileInfo(topDir.path(), collection.remoteId());
+    QVERIFY(fileInfo1_1.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // test moving mbox from maildir parent
+    Collection collection1_2;
+    collection1_2.setName(QStringLiteral("collection1_2"));
+    collection1_2.setRemoteId(QStringLiteral("collection1_2"));
+    collection1_2.setParentCollection(collection1);
+
+    job = mStore->moveCollection(collection1_2, mStore->topLevelCollection());
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection1_2.remoteId());
+    QCOMPARE(collection.parentCollection(), mStore->topLevelCollection());
+
+    fileInfo1_2.refresh();
+    QVERIFY(!fileInfo1_2.exists());
+    fileInfo1_2 = QFileInfo(topDir.path(), collection.remoteId());
+    QVERIFY(fileInfo1_2.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // test moving mbox from mbox parent
+    Collection collection2;
+    collection2.setName(QStringLiteral("collection2"));
+    collection2.setRemoteId(QStringLiteral("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+
+    Collection collection2_1;
+    collection2_1.setName(QStringLiteral("collection2_1"));
+    collection2_1.setRemoteId(QStringLiteral("collection2_1"));
+    collection2_1.setParentCollection(collection2);
+
+    job = mStore->moveCollection(collection2_1, mStore->topLevelCollection());
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection2_1.remoteId());
+    QCOMPARE(collection.parentCollection(), mStore->topLevelCollection());
+
+    fileInfo2_1.refresh();
+    QVERIFY(!fileInfo2_1.exists());
+    fileInfo2_1 = QFileInfo(topDir.path(), collection.remoteId());
+    QVERIFY(fileInfo2_1.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // test moving maildir from mbox parent
+    Collection collection2_2;
+    collection2_2.setName(QStringLiteral("collection2_2"));
+    collection2_2.setRemoteId(QStringLiteral("collection2_2"));
+    collection2_2.setParentCollection(collection2);
+
+    job = mStore->moveCollection(collection2_2, mStore->topLevelCollection());
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection2_2.remoteId());
+    QCOMPARE(collection.parentCollection(), mStore->topLevelCollection());
+
+    fileInfo2_2.refresh();
+    QVERIFY(!fileInfo2_2.exists());
+    fileInfo2_2 = QFileInfo(topDir.path(), collection.remoteId());
+    QVERIFY(fileInfo2_2.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+}
+
+void CollectionMoveTest::testMoveToMaildir()
+{
+    QDir topDir(mDir->path());
+
+    // top level dir
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), topDir.path(), QStringLiteral("collection1")));
+    QFileInfo fileInfo1(topDir, QStringLiteral("collection1"));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), topDir.path(), QStringLiteral("collection2")));
+    QFileInfo fileInfo2(topDir, QStringLiteral("collection2"));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), topDir.path(), QStringLiteral("collection3")));
+    QFileInfo fileInfo3(topDir, QStringLiteral("collection3"));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), topDir.path(), QStringLiteral("collection4")));
+    QFileInfo fileInfo4(topDir, QStringLiteral("collection4"));
+
+    // first level maildir parent
+    QDir subDir1 = topDir;
+    QVERIFY(subDir1.mkdir(QStringLiteral(".collection1.directory")));
+    QVERIFY(subDir1.cd(QStringLiteral(".collection1.directory")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), subDir1.path(), QStringLiteral("collection1_1")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), subDir1.path(), QStringLiteral("collection1_2")));
+
+    // first level mbox parent
+    QDir subDir4 = topDir;
+    QVERIFY(subDir4.mkdir(QStringLiteral(".collection4.directory")));
+    QVERIFY(subDir4.cd(QStringLiteral(".collection4.directory")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), subDir4.path(), QStringLiteral("collection4_1")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), subDir4.path(), QStringLiteral("collection4_2")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), subDir4.path(), QStringLiteral("collection4_3")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), subDir4.path(), QStringLiteral("collection4_4")));
+
+    // target maildir
+    KPIM::Maildir topLevelMd(topDir.path(), true);
+    KPIM::Maildir targetMd(topLevelMd.addSubFolder(QStringLiteral("target")), false);
+    QVERIFY(targetMd.isValid());
+    QDir subDirTarget;
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    FileStore::CollectionMoveJob *job = 0;
+    FileStore::ItemFetchJob *itemFetch = 0;
+    Collection collection;
+    const QVariant colListVar = QVariant::fromValue<Collection::List>(Collection::List());
+    QVariant var;
+    Collection::List collections;
+    Item::List items;
+    QMap<QByteArray, int> flagCounts;
+
+    Collection target;
+    target.setName(QStringLiteral("target"));
+    target.setRemoteId(QStringLiteral("target"));
+    target.setParentCollection(mStore->topLevelCollection());
+
+    // test move leaf maildir into sibling
+    Collection collection2;
+    collection2.setName(QStringLiteral("collection2"));
+    collection2.setRemoteId(QStringLiteral("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->moveCollection(collection2, target);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    subDirTarget = topDir;
+    QVERIFY(subDirTarget.cd(QStringLiteral(".target.directory")));
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection2.remoteId());
+    QCOMPARE(collection.parentCollection(), target);
+
+    fileInfo2.refresh();
+    QVERIFY(!fileInfo2.exists());
+    fileInfo2 = QFileInfo(subDirTarget, collection.remoteId());
+    QVERIFY(fileInfo2.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // test move leaf mbox into sibling
+    Collection collection3;
+    collection3.setName(QStringLiteral("collection3"));
+    collection3.setRemoteId(QStringLiteral("collection3"));
+    collection3.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->moveCollection(collection3, target);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection3.remoteId());
+    QCOMPARE(collection.parentCollection(), target);
+
+    fileInfo3.refresh();
+    QVERIFY(!fileInfo3.exists());
+    fileInfo3 = QFileInfo(subDirTarget, collection.remoteId());
+    QVERIFY(fileInfo3.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // test move maildir with subtree into sibling
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    // load sub collection index data to check for correct cache updates
+    Collection collection1_1;
+    collection1_1.setName(QStringLiteral("collection1_1"));
+    collection1_1.setRemoteId(QStringLiteral("collection1_1"));
+    collection1_1.setParentCollection(collection1);
+    itemFetch = mStore->fetchItems(collection1_1);
+    QVERIFY(itemFetch->exec());
+
+    Collection collection1_2;
+    collection1_2.setName(QStringLiteral("collection1_2"));
+    collection1_2.setRemoteId(QStringLiteral("collection1_2"));
+    collection1_2.setParentCollection(collection1);
+    itemFetch = mStore->fetchItems(collection1_2);
+    QVERIFY(itemFetch->exec());
+
+    job = mStore->moveCollection(collection1, target);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection1.remoteId());
+    QCOMPARE(collection.parentCollection(), target);
+
+    fileInfo1.refresh();
+    QVERIFY(!fileInfo1.exists());
+    fileInfo1 = QFileInfo(subDirTarget, collection.remoteId());
+    QVERIFY(fileInfo1.exists());
+    QVERIFY(!subDir1.exists());
+    subDir1 = subDirTarget;
+    QVERIFY(subDir1.cd(QStringLiteral(".collection1.directory")));
+    QCOMPARE(subDir1.entryList(QStringList() << QStringLiteral("collection*")),
+             QStringList() << QStringLiteral("collection1_1") << QStringLiteral("collection1_2"));
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // check for children cache path updates
+    collection1.setParentCollection(target);
+    collection1_1.setParentCollection(collection1);
+    collection1_2.setParentCollection(collection1);
+
+    itemFetch = mStore->fetchItems(collection1_1);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    itemFetch = mStore->fetchItems(collection1_2);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // test move mbox with subtree into sibling
+    Collection collection4;
+    collection4.setName(QStringLiteral("collection4"));
+    collection4.setRemoteId(QStringLiteral("collection4"));
+    collection4.setParentCollection(mStore->topLevelCollection());
+
+    // load sub collection index data to check for correct cache updates
+    Collection collection4_1;
+    collection4_1.setName(QStringLiteral("collection4_1"));
+    collection4_1.setRemoteId(QStringLiteral("collection4_1"));
+    collection4_1.setParentCollection(collection4);
+    itemFetch = mStore->fetchItems(collection4_1);
+    QVERIFY(itemFetch->exec());
+
+    Collection collection4_2;
+    collection4_2.setName(QStringLiteral("collection4_2"));
+    collection4_2.setRemoteId(QStringLiteral("collection4_2"));
+    collection4_2.setParentCollection(collection4);
+    itemFetch = mStore->fetchItems(collection4_2);
+    QVERIFY(itemFetch->exec());
+
+    job = mStore->moveCollection(collection4, target);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection4.remoteId());
+    QCOMPARE(collection.parentCollection(), target);
+
+    fileInfo4.refresh();
+    QVERIFY(!fileInfo4.exists());
+    fileInfo4 = QFileInfo(subDirTarget, collection.remoteId());
+    QVERIFY(fileInfo4.exists());
+    QVERIFY(!subDir4.exists());
+    subDir4 = subDirTarget;
+    QVERIFY(subDir4.cd(QStringLiteral(".collection4.directory")));
+    QCOMPARE(subDir4.entryList(QStringList() << QStringLiteral("collection*")),
+             QStringList() << QStringLiteral("collection4_1") << QStringLiteral("collection4_2")
+             << QStringLiteral("collection4_3") << QStringLiteral("collection4_4")
+            );
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // check for children cache path updates
+    collection4.setParentCollection(target);
+    collection4_1.setParentCollection(collection4);
+    collection4_2.setParentCollection(collection4);
+
+    itemFetch = mStore->fetchItems(collection4_1);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    itemFetch = mStore->fetchItems(collection4_2);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // move from parent maildir to parent's sibling
+    collection2.setParentCollection(target);
+
+    job = mStore->moveCollection(collection1_1, collection2);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    QDir subDir2 = subDirTarget;
+    QVERIFY(subDir2.cd(QStringLiteral(".collection2.directory")));
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection1_1.remoteId());
+    QCOMPARE(collection.parentCollection(), collection2);
+
+    QFileInfo fileInfo1_1(subDir1, collection.remoteId());
+    QVERIFY(!fileInfo1_1.exists());
+    fileInfo1_1 = QFileInfo(subDir2, collection.remoteId());
+    QVERIFY(fileInfo1_1.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // move from parent maildir to parent's sibling
+    job = mStore->moveCollection(collection1_2, collection2);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection1_2.remoteId());
+    QCOMPARE(collection.parentCollection(), collection2);
+
+    QFileInfo fileInfo1_2(subDir1, collection.remoteId());
+    QVERIFY(!fileInfo1_2.exists());
+    fileInfo1_2 = QFileInfo(subDir2, collection.remoteId());
+    QVERIFY(fileInfo1_2.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // move from parent mbox to parent's sibling
+    job = mStore->moveCollection(collection4_1, collection2);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection4_1.remoteId());
+    QCOMPARE(collection.parentCollection(), collection2);
+
+    QFileInfo fileInfo4_1(subDir4, collection.remoteId());
+    QVERIFY(!fileInfo4_1.exists());
+    fileInfo4_1 = QFileInfo(subDir2, collection.remoteId());
+    QVERIFY(fileInfo4_1.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // move from parent mbox to parent's sibling
+    job = mStore->moveCollection(collection4_2, collection2);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection4_2.remoteId());
+    QCOMPARE(collection.parentCollection(), collection2);
+
+    QFileInfo fileInfo4_2(subDir4, collection.remoteId());
+    QVERIFY(!fileInfo4_2.exists());
+    fileInfo4_2 = QFileInfo(subDir2, collection.remoteId());
+    QVERIFY(fileInfo4_2.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // move from parent maildir to grandparent
+    collection1_1.setParentCollection(collection2);
+
+    job = mStore->moveCollection(collection1_1, target);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection1_1.remoteId());
+    QCOMPARE(collection.parentCollection(), target);
+
+    fileInfo1_1.refresh();
+    QVERIFY(!fileInfo1_1.exists());
+    fileInfo1_1 = QFileInfo(subDirTarget, collection.remoteId());
+    QVERIFY(fileInfo1_1.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // move from parent maildir to grandparent
+    collection1_2.setParentCollection(collection2);
+    job = mStore->moveCollection(collection1_2, target);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection1_2.remoteId());
+    QCOMPARE(collection.parentCollection(), target);
+
+    fileInfo1_2.refresh();
+    QVERIFY(!fileInfo1_2.exists());
+    fileInfo1_2 = QFileInfo(subDirTarget, collection.remoteId());
+    QVERIFY(fileInfo1_2.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // move from parent mbox to grandparent
+    Collection collection4_3;
+    collection4_3.setName(QStringLiteral("collection4_3"));
+    collection4_3.setRemoteId(QStringLiteral("collection4_3"));
+    collection4_3.setParentCollection(collection4);
+
+    job = mStore->moveCollection(collection4_3, target);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection4_3.remoteId());
+    QCOMPARE(collection.parentCollection(), target);
+
+    QFileInfo fileInfo4_3(subDir4, collection.remoteId());
+    QVERIFY(!fileInfo4_3.exists());
+    fileInfo4_3 = QFileInfo(subDirTarget, collection.remoteId());
+    QVERIFY(fileInfo4_3.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // move from parent mbox to grandparent
+    Collection collection4_4;
+    collection4_4.setName(QStringLiteral("collection4_4"));
+    collection4_4.setRemoteId(QStringLiteral("collection4_4"));
+    collection4_4.setParentCollection(collection4);
+
+    job = mStore->moveCollection(collection4_4, target);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection4_4.remoteId());
+    QCOMPARE(collection.parentCollection(), target);
+
+    QFileInfo fileInfo4_4(subDir4, collection.remoteId());
+    QVERIFY(!fileInfo4_4.exists());
+    fileInfo4_4 = QFileInfo(subDirTarget, collection.remoteId());
+    QVERIFY(fileInfo4_4.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // move from maildir to grandchild
+    collection1_1.setParentCollection(target);
+
+    job = mStore->moveCollection(collection1_1, collection2);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection1_1.remoteId());
+    QCOMPARE(collection.parentCollection(), collection2);
+
+    fileInfo1_1.refresh();
+    QVERIFY(!fileInfo1_1.exists());
+    fileInfo1_1 = QFileInfo(subDir2, collection.remoteId());
+    QVERIFY(fileInfo1_1.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // move from maildir to grandchild
+    collection1_2.setParentCollection(target);
+
+    job = mStore->moveCollection(collection1_2, collection2);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection1_2.remoteId());
+    QCOMPARE(collection.parentCollection(), collection2);
+
+    fileInfo1_2.refresh();
+    QVERIFY(!fileInfo1_2.exists());
+    fileInfo1_2 = QFileInfo(subDir2, collection.remoteId());
+    QVERIFY(fileInfo1_2.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+}
+
+void CollectionMoveTest::testMoveToMBox()
+{
+    QDir topDir(mDir->path());
+
+    // top level dir
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), topDir.path(), QStringLiteral("collection1")));
+    QFileInfo fileInfo1(topDir, QStringLiteral("collection1"));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), topDir.path(), QStringLiteral("collection2")));
+    QFileInfo fileInfo2(topDir, QStringLiteral("collection2"));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), topDir.path(), QStringLiteral("collection3")));
+    QFileInfo fileInfo3(topDir, QStringLiteral("collection3"));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), topDir.path(), QStringLiteral("collection4")));
+    QFileInfo fileInfo4(topDir, QStringLiteral("collection4"));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), topDir.path(), QStringLiteral("collection5")));
+    QFileInfo fileInfo5(topDir, QStringLiteral("collection5"));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), topDir.path(), QStringLiteral("collection6")));
+    QFileInfo fileInfo6(topDir, QStringLiteral("collection6"));
+
+    // first level maildir parent
+    QDir subDir1 = topDir;
+    QVERIFY(subDir1.mkdir(QStringLiteral(".collection1.directory")));
+    QVERIFY(subDir1.cd(QStringLiteral(".collection1.directory")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), subDir1.path(), QStringLiteral("collection1_1")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), subDir1.path(), QStringLiteral("collection1_2")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), subDir1.path(), QStringLiteral("collection1_3")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), subDir1.path(), QStringLiteral("collection1_4")));
+
+    // first level mbox parent
+    QDir subDir4 = topDir;
+    QVERIFY(subDir4.mkdir(QStringLiteral(".collection4.directory")));
+    QVERIFY(subDir4.cd(QStringLiteral(".collection4.directory")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), subDir4.path(), QStringLiteral("collection4_1")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), subDir4.path(), QStringLiteral("collection4_2")));
+
+    // target mbox
+    QFileInfo fileInfoTarget(topDir.path(), QStringLiteral("target"));
+    QFile fileTarget(fileInfoTarget.absoluteFilePath());
+    QVERIFY(fileTarget.open(QIODevice::WriteOnly));
+    fileTarget.close();
+    QVERIFY(fileInfoTarget.exists());
+
+    QDir subDirTarget;
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    FileStore::CollectionMoveJob *job = 0;
+    FileStore::ItemFetchJob *itemFetch = 0;
+    Collection collection;
+    const QVariant colListVar = QVariant::fromValue<Collection::List>(Collection::List());
+    QVariant var;
+    Collection::List collections;
+    Item::List items;
+    QMap<QByteArray, int> flagCounts;
+
+    Collection target;
+    target.setName(QStringLiteral("target"));
+    target.setRemoteId(QStringLiteral("target"));
+    target.setParentCollection(mStore->topLevelCollection());
+
+    // test move leaf maildir into sibling
+    Collection collection2;
+    collection2.setName(QStringLiteral("collection2"));
+    collection2.setRemoteId(QStringLiteral("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->moveCollection(collection2, target);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    subDirTarget = topDir;
+    QVERIFY(subDirTarget.cd(QStringLiteral(".target.directory")));
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection2.remoteId());
+    QCOMPARE(collection.parentCollection(), target);
+
+    fileInfo2.refresh();
+    QVERIFY(!fileInfo2.exists());
+    fileInfo2 = QFileInfo(subDirTarget, collection.remoteId());
+    QVERIFY(fileInfo2.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // test move leaf mbox into sibling
+    Collection collection3;
+    collection3.setName(QStringLiteral("collection3"));
+    collection3.setRemoteId(QStringLiteral("collection3"));
+    collection3.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->moveCollection(collection3, target);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection3.remoteId());
+    QCOMPARE(collection.parentCollection(), target);
+
+    fileInfo3.refresh();
+    QVERIFY(!fileInfo3.exists());
+    fileInfo3 = QFileInfo(subDirTarget, collection.remoteId());
+    QVERIFY(fileInfo3.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // test move leaf mbox into sibling without subtree
+    Collection collection5;
+    collection5.setName(QStringLiteral("collection5"));
+    collection5.setRemoteId(QStringLiteral("collection5"));
+    collection5.setParentCollection(mStore->topLevelCollection());
+
+    Collection collection6;
+    collection6.setName(QStringLiteral("collection6"));
+    collection6.setRemoteId(QStringLiteral("collection6"));
+    collection6.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->moveCollection(collection5, collection6);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection5.remoteId());
+    QCOMPARE(collection.parentCollection(), collection6);
+
+    fileInfo5.refresh();
+    QVERIFY(!fileInfo5.exists());
+    QDir subDir6 = topDir;
+    QVERIFY(subDir6.cd(QStringLiteral(".collection6.directory")));
+    fileInfo5 = QFileInfo(subDir6, collection.remoteId());
+    QVERIFY(fileInfo5.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // test move maildir with subtree into sibling
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    // load sub collection index data to check for correct cache updates
+    Collection collection1_1;
+    collection1_1.setName(QStringLiteral("collection1_1"));
+    collection1_1.setRemoteId(QStringLiteral("collection1_1"));
+    collection1_1.setParentCollection(collection1);
+    itemFetch = mStore->fetchItems(collection1_1);
+    QVERIFY(itemFetch->exec());
+
+    Collection collection1_2;
+    collection1_2.setName(QStringLiteral("collection1_2"));
+    collection1_2.setRemoteId(QStringLiteral("collection1_2"));
+    collection1_2.setParentCollection(collection1);
+    itemFetch = mStore->fetchItems(collection1_2);
+    QVERIFY(itemFetch->exec());
+
+    job = mStore->moveCollection(collection1, target);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection1.remoteId());
+    QCOMPARE(collection.parentCollection(), target);
+
+    fileInfo1.refresh();
+    QVERIFY(!fileInfo1.exists());
+    fileInfo1 = QFileInfo(subDirTarget, collection.remoteId());
+    QVERIFY(fileInfo1.exists());
+    QVERIFY(!subDir1.exists());
+    subDir1 = subDirTarget;
+    QVERIFY(subDir1.cd(QStringLiteral(".collection1.directory")));
+    QCOMPARE(subDir1.entryList(QStringList() << QStringLiteral("collection*")),
+             QStringList() << QStringLiteral("collection1_1") << QStringLiteral("collection1_2")
+             << QStringLiteral("collection1_3") << QStringLiteral("collection1_4")
+            );
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // check for children cache path updates
+    collection1.setParentCollection(target);
+    collection1_1.setParentCollection(collection1);
+    collection1_2.setParentCollection(collection1);
+
+    itemFetch = mStore->fetchItems(collection1_1);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    itemFetch = mStore->fetchItems(collection1_2);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // test move mbox with subtree into sibling
+    Collection collection4;
+    collection4.setName(QStringLiteral("collection4"));
+    collection4.setRemoteId(QStringLiteral("collection4"));
+    collection4.setParentCollection(mStore->topLevelCollection());
+
+    // load sub collection index data to check for correct cache updates
+    Collection collection4_1;
+    collection4_1.setName(QStringLiteral("collection4_1"));
+    collection4_1.setRemoteId(QStringLiteral("collection4_1"));
+    collection4_1.setParentCollection(collection4);
+    itemFetch = mStore->fetchItems(collection4_1);
+    QVERIFY(itemFetch->exec());
+
+    Collection collection4_2;
+    collection4_2.setName(QStringLiteral("collection4_2"));
+    collection4_2.setRemoteId(QStringLiteral("collection4_2"));
+    collection4_2.setParentCollection(collection4);
+    itemFetch = mStore->fetchItems(collection4_2);
+    QVERIFY(itemFetch->exec());
+
+    job = mStore->moveCollection(collection4, target);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection4.remoteId());
+    QCOMPARE(collection.parentCollection(), target);
+
+    fileInfo4.refresh();
+    QVERIFY(!fileInfo4.exists());
+    fileInfo4 = QFileInfo(subDirTarget, collection.remoteId());
+    QVERIFY(fileInfo4.exists());
+    QVERIFY(!subDir4.exists());
+    subDir4 = subDirTarget;
+    QVERIFY(subDir4.cd(QStringLiteral(".collection4.directory")));
+    QCOMPARE(subDir4.entryList(QStringList() << QStringLiteral("collection*")),
+             QStringList() << QStringLiteral("collection4_1") << QStringLiteral("collection4_2")
+            );
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // check for children cache path updates
+    collection4.setParentCollection(target);
+    collection4_1.setParentCollection(collection4);
+    collection4_2.setParentCollection(collection4);
+
+    itemFetch = mStore->fetchItems(collection4_1);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    itemFetch = mStore->fetchItems(collection4_2);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // move from parent maildir to parent's sibling
+    collection3.setParentCollection(target);
+
+    job = mStore->moveCollection(collection1_1, collection3);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    QDir subDir3 = subDirTarget;
+    QVERIFY(subDir3.cd(QStringLiteral(".collection3.directory")));
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection1_1.remoteId());
+    QCOMPARE(collection.parentCollection(), collection3);
+
+    QFileInfo fileInfo1_1(subDir1, collection.remoteId());
+    QVERIFY(!fileInfo1_1.exists());
+    fileInfo1_1 = QFileInfo(subDir3, collection.remoteId());
+    QVERIFY(fileInfo1_1.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // move from parent maildir to parent's sibling
+    job = mStore->moveCollection(collection1_2, collection3);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection1_2.remoteId());
+    QCOMPARE(collection.parentCollection(), collection3);
+
+    QFileInfo fileInfo1_2(subDir1, collection.remoteId());
+    QVERIFY(!fileInfo1_2.exists());
+    fileInfo1_2 = QFileInfo(subDir3, collection.remoteId());
+    QVERIFY(fileInfo1_2.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // move from parent mbox to parent's sibling
+    job = mStore->moveCollection(collection4_1, collection3);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection4_1.remoteId());
+    QCOMPARE(collection.parentCollection(), collection3);
+
+    QFileInfo fileInfo4_1(subDir4, collection.remoteId());
+    QVERIFY(!fileInfo4_1.exists());
+    fileInfo4_1 = QFileInfo(subDir3, collection.remoteId());
+    QVERIFY(fileInfo4_1.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // move from parent mbox to parent's sibling
+    job = mStore->moveCollection(collection4_2, collection3);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection4_2.remoteId());
+    QCOMPARE(collection.parentCollection(), collection3);
+
+    QFileInfo fileInfo4_2(subDir4, collection.remoteId());
+    QVERIFY(!fileInfo4_2.exists());
+    fileInfo4_2 = QFileInfo(subDir3, collection.remoteId());
+    QVERIFY(fileInfo4_2.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // move from parent mbox to grandparent
+    collection1_1.setParentCollection(collection3);
+
+    job = mStore->moveCollection(collection1_1, target);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection1_1.remoteId());
+    QCOMPARE(collection.parentCollection(), target);
+
+    fileInfo1_1.refresh();
+    QVERIFY(!fileInfo1_1.exists());
+    fileInfo1_1 = QFileInfo(subDirTarget, collection.remoteId());
+    QVERIFY(fileInfo1_1.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // move from parent mbox to grandparent
+    collection1_2.setParentCollection(collection3);
+    job = mStore->moveCollection(collection1_2, target);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection1_2.remoteId());
+    QCOMPARE(collection.parentCollection(), target);
+
+    fileInfo1_2.refresh();
+    QVERIFY(!fileInfo1_2.exists());
+    fileInfo1_2 = QFileInfo(subDirTarget, collection.remoteId());
+    QVERIFY(fileInfo1_2.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // move from parent maildir to grandparent
+    Collection collection1_3;
+    collection1_3.setName(QStringLiteral("collection1_3"));
+    collection1_3.setRemoteId(QStringLiteral("collection1_3"));
+    collection1_3.setParentCollection(collection1);
+
+    job = mStore->moveCollection(collection1_3, target);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection1_3.remoteId());
+    QCOMPARE(collection.parentCollection(), target);
+
+    QFileInfo fileInfo1_3(subDir1, collection.remoteId());
+    QVERIFY(!fileInfo1_3.exists());
+    fileInfo1_3 = QFileInfo(subDirTarget, collection.remoteId());
+    QVERIFY(fileInfo1_3.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // move from parent maildir to grandparent
+    Collection collection1_4;
+    collection1_4.setName(QStringLiteral("collection1_4"));
+    collection1_4.setRemoteId(QStringLiteral("collection1_4"));
+    collection1_4.setParentCollection(collection1);
+
+    job = mStore->moveCollection(collection1_4, target);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection1_4.remoteId());
+    QCOMPARE(collection.parentCollection(), target);
+
+    QFileInfo fileInfo1_4(subDir1, collection.remoteId());
+    QVERIFY(!fileInfo1_4.exists());
+    fileInfo1_4 = QFileInfo(subDirTarget, collection.remoteId());
+    QVERIFY(fileInfo1_4.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // move from mbox to grandchild
+    collection1_1.setParentCollection(target);
+
+    job = mStore->moveCollection(collection1_1, collection3);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection1_1.remoteId());
+    QCOMPARE(collection.parentCollection(), collection3);
+
+    fileInfo1_1.refresh();
+    QVERIFY(!fileInfo1_1.exists());
+    fileInfo1_1 = QFileInfo(subDir3, collection.remoteId());
+    QVERIFY(fileInfo1_1.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    // move from maildir to grandchild
+    collection1_2.setParentCollection(target);
+
+    job = mStore->moveCollection(collection1_2, collection3);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collection = job->collection();
+    QCOMPARE(collection.remoteId(), collection1_2.remoteId());
+    QCOMPARE(collection.parentCollection(), collection3);
+
+    fileInfo1_2.refresh();
+    QVERIFY(!fileInfo1_2.exists());
+    fileInfo1_2 = QFileInfo(subDir3, collection.remoteId());
+    QVERIFY(fileInfo1_2.exists());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 4);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+}
+
+QTEST_MAIN(CollectionMoveTest)
+
+#include "collectionmovetest.moc"
+
diff --git a/resources/mixedmaildir/autotests/data/.dimap.index b/resources/mixedmaildir/autotests/data/.dimap.index
new file mode 100644 (file)
index 0000000..11d05bd
Binary files /dev/null and b/resources/mixedmaildir/autotests/data/.dimap.index differ
diff --git a/resources/mixedmaildir/autotests/data/.maildir-tagged.index b/resources/mixedmaildir/autotests/data/.maildir-tagged.index
new file mode 100644 (file)
index 0000000..297fb6e
Binary files /dev/null and b/resources/mixedmaildir/autotests/data/.maildir-tagged.index differ
diff --git a/resources/mixedmaildir/autotests/data/.maildir.index b/resources/mixedmaildir/autotests/data/.maildir.index
new file mode 100644 (file)
index 0000000..cfd000a
Binary files /dev/null and b/resources/mixedmaildir/autotests/data/.maildir.index differ
diff --git a/resources/mixedmaildir/autotests/data/.mbox-tagged.index b/resources/mixedmaildir/autotests/data/.mbox-tagged.index
new file mode 100644 (file)
index 0000000..a427d51
Binary files /dev/null and b/resources/mixedmaildir/autotests/data/.mbox-tagged.index differ
diff --git a/resources/mixedmaildir/autotests/data/.mbox-unpurged.index b/resources/mixedmaildir/autotests/data/.mbox-unpurged.index
new file mode 100644 (file)
index 0000000..165fac0
Binary files /dev/null and b/resources/mixedmaildir/autotests/data/.mbox-unpurged.index differ
diff --git a/resources/mixedmaildir/autotests/data/.mbox.index b/resources/mixedmaildir/autotests/data/.mbox.index
new file mode 100644 (file)
index 0000000..f28eb9e
Binary files /dev/null and b/resources/mixedmaildir/autotests/data/.mbox.index differ
diff --git a/resources/mixedmaildir/autotests/data/README b/resources/mixedmaildir/autotests/data/README
new file mode 100644 (file)
index 0000000..bd8bb43
--- /dev/null
@@ -0,0 +1,38 @@
+Description of contained Test data
+==================================
+
+maildir/dimap message filenames that contained ':' had it replaced with '_'
+
+1) dimap: 4 messages + index
+  - message 1 flagged important
+  - message 2 flagged unread
+  - message 3 flagged new, empty body
+  - message 4 flagged task, empty subject
+
+2) maildir: 4 messages + index
+  - message 1 flagged important
+  - message 2 flagged unread
+  - message 3 flagged new, empty body
+  - message 4 flagged task, empty subject
+
+3) mbox: 4 messages + index
+  - message 1 flagged unread
+  - message 2 flagged task, empty subject
+  - message 3 flagged new, empty body
+  - message 4 flagged important
+
+4) maildir-tagged: 4 messages + index
+  - message 1 flagged important, tag1
+  - message 2 flagged unread, tag2
+  - message 3 flagged new, empty body, tag 3
+  - message 4 flagged task, empty subject
+
+5) mbox-tagged: 4 messages + index
+  - message 1 flagged unread, tag2
+  - message 2 flagged task, empty subject
+  - message 3 flagged new, empty body, tag 3
+  - message 4 flagged important, tag1
+
+6) mbox-unpurged: 4 messages + index with entries for the first two
+  - simulating an unpurged mbox: all messages in mbox, no index entries for
+    the two messages in the middle
diff --git a/resources/mixedmaildir/autotests/data/dimap/cur/1279980064.4595.LUBVK b/resources/mixedmaildir/autotests/data/dimap/cur/1279980064.4595.LUBVK
new file mode 100644 (file)
index 0000000..cf6113d
--- /dev/null
@@ -0,0 +1,46 @@
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:51:39 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx102) with SMTP; 24 Jul 2010 15:51:39 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 5074095366D
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id DEC1195366F
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id C668A95366D
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id 9EEB53103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id 8367A3103EEE
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Subject: Test 2
+Date: Sat, 24 Jul 2010 15:51:16 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241551.16855.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iL/6eeEA7/vGXc8Az2zQ5lkNsVrprUw5ljr293uKsrVdFBp1i
+ k5eF2TKdovp4qtfEvYkwJlTyCJPnhg0xAPWnM7rhY/+LZUrte1UAr5224vU33NgWA8bmceIWcEOY
+ UsgKg==V1;
+Status: RO
+X-Status: U
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+X-UID: 10
+
+Body of Test 2
diff --git a/resources/mixedmaildir/autotests/data/dimap/cur/1279980064.4595.RTmAd_2,S b/resources/mixedmaildir/autotests/data/dimap/cur/1279980064.4595.RTmAd_2,S
new file mode 100644 (file)
index 0000000..761648a
--- /dev/null
@@ -0,0 +1,45 @@
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:52:14 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx036) with SMTP; 24 Jul 2010 15:52:14 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 3536595366D
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:14 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 101AC95366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:14 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id ED25794D9DA
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:13 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id C2DA93103EEB
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:13 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id AE2D13103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:13 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Date: Sat, 24 Jul 2010 15:52:00 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241552.01232.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iM2SSXdnXxuLgAcAvPC3Xr5Z6OPcGoHnWX0YgscMEgMNa/UP6
+ 0PsZjBktxR5hqB4N3jaDTD+60EUFg8bPz1GGvD1YActDYJympMTApbcs85zthOivOU3SH3UvpiHN
+ TRN8A==V1;
+Status: RO
+X-Status: RK
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+X-UID: 11
+
+Body of Test 4
diff --git a/resources/mixedmaildir/autotests/data/dimap/cur/1279980064.4595.g8PCJ b/resources/mixedmaildir/autotests/data/dimap/cur/1279980064.4595.g8PCJ
new file mode 100644 (file)
index 0000000..066bbf1
--- /dev/null
@@ -0,0 +1,46 @@
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:51:39 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx106) with SMTP; 24 Jul 2010 15:51:39 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id A6DEC95366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 6305495366F
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id 4D0C995366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id 21F263103EEB
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id 06BF13103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Subject: Test 3
+Date: Sat, 24 Jul 2010 15:51:36 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241551.37547.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iL/6eeEA7/vGXc8Az2zQ5lkNsVrprUw5ljr293uKsrVdFBp1i
+ k5eF2TKdovp4qtfEvYkwJlTyCJPnhg0xAPWnM7rhY/+LZUrte1UAr5224vU33NgWA8bmceIWcEOY
+ UsgKg==V1;
+Status: R
+X-Status: N
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+X-UID: 12
+
+
diff --git a/resources/mixedmaildir/autotests/data/dimap/cur/1279980064.4595.qs6V9_2,S b/resources/mixedmaildir/autotests/data/dimap/cur/1279980064.4595.qs6V9_2,S
new file mode 100644 (file)
index 0000000..dcaa004
--- /dev/null
@@ -0,0 +1,48 @@
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:51:41 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx033) with SMTP; 24 Jul 2010 15:51:41 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id BD28395366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:40 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id CA62095366E
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id B5E0195366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id 754B03103EEB
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id 545963103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Subject: Test 1
+Date: Sat, 24 Jul 2010 15:50:45 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241550.46907.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iL/6eeEA7/vGXc8Az2zQ5lkNsVrprUw5ljr293uKsrVdFBp1i
+ k5eF2TKdovp4qtfEvYkwJlTyCJPnhg0xAPWnM7rhY/+LZUrte1UAr5224vU33NgWA8bmceIWcEOY
+ UsgKg==V1;
+Status: RO
+X-Status: RG
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+X-UID: 13
+
+Body
+of
+Test 1
diff --git a/resources/mixedmaildir/autotests/data/dimap/new/.keep b/resources/mixedmaildir/autotests/data/dimap/new/.keep
new file mode 100644 (file)
index 0000000..2f1d07a
--- /dev/null
@@ -0,0 +1 @@
+force git to include this file
diff --git a/resources/mixedmaildir/autotests/data/dimap/tmp/.keep b/resources/mixedmaildir/autotests/data/dimap/tmp/.keep
new file mode 100644 (file)
index 0000000..2f1d07a
--- /dev/null
@@ -0,0 +1 @@
+force git to include this file
diff --git a/resources/mixedmaildir/autotests/data/maildir-tagged/cur/1279982188.18722.6qZsA_2,S b/resources/mixedmaildir/autotests/data/maildir-tagged/cur/1279982188.18722.6qZsA_2,S
new file mode 100644 (file)
index 0000000..c9040b6
--- /dev/null
@@ -0,0 +1,45 @@
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:52:14 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx036) with SMTP; 24 Jul 2010 15:52:14 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 3536595366D
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:14 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 101AC95366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:14 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id ED25794D9DA
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:13 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id C2DA93103EEB
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:13 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id AE2D13103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:13 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Date: Sat, 24 Jul 2010 15:52:00 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241552.01232.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iM2SSXdnXxuLgAcAvPC3Xr5Z6OPcGoHnWX0YgscMEgMNa/UP6
+ 0PsZjBktxR5hqB4N3jaDTD+60EUFg8bPz1GGvD1YActDYJympMTApbcs85zthOivOU3SH3UvpiHN
+ TRN8A==V1;
+Status: RO
+X-Status: RK
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+Body of Test 4
+
diff --git a/resources/mixedmaildir/autotests/data/maildir-tagged/cur/1279982188.18722.Xdz3R_2,S b/resources/mixedmaildir/autotests/data/maildir-tagged/cur/1279982188.18722.Xdz3R_2,S
new file mode 100644 (file)
index 0000000..e6ac84a
--- /dev/null
@@ -0,0 +1,46 @@
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:51:39 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx102) with SMTP; 24 Jul 2010 15:51:39 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 5074095366D
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id DEC1195366F
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id C668A95366D
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id 9EEB53103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id 8367A3103EEE
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Subject: Test 2
+Date: Sat, 24 Jul 2010 15:51:16 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241551.16855.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iL/6eeEA7/vGXc8Az2zQ5lkNsVrprUw5ljr293uKsrVdFBp1i
+ k5eF2TKdovp4qtfEvYkwJlTyCJPnhg0xAPWnM7rhY/+LZUrte1UAr5224vU33NgWA8bmceIWcEOY
+ UsgKg==V1;
+Status: RO
+X-Status: U
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+Body of Test 2
+
diff --git a/resources/mixedmaildir/autotests/data/maildir-tagged/cur/1279982188.18722.f0l49_2,S b/resources/mixedmaildir/autotests/data/maildir-tagged/cur/1279982188.18722.f0l49_2,S
new file mode 100644 (file)
index 0000000..8476dda
--- /dev/null
@@ -0,0 +1,45 @@
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:51:39 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx106) with SMTP; 24 Jul 2010 15:51:39 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id A6DEC95366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 6305495366F
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id 4D0C995366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id 21F263103EEB
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id 06BF13103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Subject: Test 3
+Date: Sat, 24 Jul 2010 15:51:36 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241551.37547.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iL/6eeEA7/vGXc8Az2zQ5lkNsVrprUw5ljr293uKsrVdFBp1i
+ k5eF2TKdovp4qtfEvYkwJlTyCJPnhg0xAPWnM7rhY/+LZUrte1UAr5224vU33NgWA8bmceIWcEOY
+ UsgKg==V1;
+Status: R
+X-Status: N
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+
diff --git a/resources/mixedmaildir/autotests/data/maildir-tagged/cur/1279982188.18722.kwx1b_2,S b/resources/mixedmaildir/autotests/data/maildir-tagged/cur/1279982188.18722.kwx1b_2,S
new file mode 100644 (file)
index 0000000..493ccd3
--- /dev/null
@@ -0,0 +1,47 @@
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:51:41 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx033) with SMTP; 24 Jul 2010 15:51:41 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id BD28395366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:40 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id CA62095366E
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id B5E0195366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id 754B03103EEB
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id 545963103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Subject: Test 1
+Date: Sat, 24 Jul 2010 15:50:45 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241550.46907.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iL/6eeEA7/vGXc8Az2zQ5lkNsVrprUw5ljr293uKsrVdFBp1i
+ k5eF2TKdovp4qtfEvYkwJlTyCJPnhg0xAPWnM7rhY/+LZUrte1UAr5224vU33NgWA8bmceIWcEOY
+ UsgKg==V1;
+Status: RO
+X-Status: RG
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+Body
+of
+Test 1
diff --git a/resources/mixedmaildir/autotests/data/maildir-tagged/new/.keep b/resources/mixedmaildir/autotests/data/maildir-tagged/new/.keep
new file mode 100644 (file)
index 0000000..2f1d07a
--- /dev/null
@@ -0,0 +1 @@
+force git to include this file
diff --git a/resources/mixedmaildir/autotests/data/maildir-tagged/tmp/.keep b/resources/mixedmaildir/autotests/data/maildir-tagged/tmp/.keep
new file mode 100644 (file)
index 0000000..2f1d07a
--- /dev/null
@@ -0,0 +1 @@
+force git to include this file
diff --git a/resources/mixedmaildir/autotests/data/maildir/cur/1279979617.4595.bwXSm b/resources/mixedmaildir/autotests/data/maildir/cur/1279979617.4595.bwXSm
new file mode 100644 (file)
index 0000000..8476dda
--- /dev/null
@@ -0,0 +1,45 @@
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:51:39 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx106) with SMTP; 24 Jul 2010 15:51:39 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id A6DEC95366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 6305495366F
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id 4D0C995366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id 21F263103EEB
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id 06BF13103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Subject: Test 3
+Date: Sat, 24 Jul 2010 15:51:36 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241551.37547.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iL/6eeEA7/vGXc8Az2zQ5lkNsVrprUw5ljr293uKsrVdFBp1i
+ k5eF2TKdovp4qtfEvYkwJlTyCJPnhg0xAPWnM7rhY/+LZUrte1UAr5224vU33NgWA8bmceIWcEOY
+ UsgKg==V1;
+Status: R
+X-Status: N
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+
diff --git a/resources/mixedmaildir/autotests/data/maildir/cur/1279979618.4595.CStza_2,S b/resources/mixedmaildir/autotests/data/maildir/cur/1279979618.4595.CStza_2,S
new file mode 100644 (file)
index 0000000..347c199
--- /dev/null
@@ -0,0 +1,47 @@
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:51:41 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx033) with SMTP; 24 Jul 2010 15:51:41 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id BD28395366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:40 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id CA62095366E
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id B5E0195366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id 754B03103EEB
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id 545963103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Subject: Test 1
+Date: Sat, 24 Jul 2010 15:50:45 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241550.46907.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iL/6eeEA7/vGXc8Az2zQ5lkNsVrprUw5ljr293uKsrVdFBp1i
+ k5eF2TKdovp4qtfEvYkwJlTyCJPnhg0xAPWnM7rhY/+LZUrte1UAr5224vU33NgWA8bmceIWcEOY
+ UsgKg==V1;
+Status: RO
+X-Status: R
+X-KMail-EncryptionState: N
+X-KMail-SignatureState: N
+X-KMail-MDN-Sent:  
+
+Body
+of
+Test 1
diff --git a/resources/mixedmaildir/autotests/data/maildir/cur/1279979618.4595.DUl0I_2,S b/resources/mixedmaildir/autotests/data/maildir/cur/1279979618.4595.DUl0I_2,S
new file mode 100644 (file)
index 0000000..ac6afa9
--- /dev/null
@@ -0,0 +1,44 @@
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:52:14 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx036) with SMTP; 24 Jul 2010 15:52:14 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 3536595366D
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:14 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 101AC95366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:14 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id ED25794D9DA
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:13 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id C2DA93103EEB
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:13 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id AE2D13103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:13 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Date: Sat, 24 Jul 2010 15:52:00 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241552.01232.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iM2SSXdnXxuLgAcAvPC3Xr5Z6OPcGoHnWX0YgscMEgMNa/UP6
+ 0PsZjBktxR5hqB4N3jaDTD+60EUFg8bPz1GGvD1YActDYJympMTApbcs85zthOivOU3SH3UvpiHN
+ TRN8A==V1;
+Status: RO
+X-Status: R
+X-KMail-EncryptionState: N
+X-KMail-SignatureState: N
+X-KMail-MDN-Sent:  
+
+Body of Test 4
diff --git a/resources/mixedmaildir/autotests/data/maildir/cur/1279979618.4595.pY5ny b/resources/mixedmaildir/autotests/data/maildir/cur/1279979618.4595.pY5ny
new file mode 100644 (file)
index 0000000..e6fd345
--- /dev/null
@@ -0,0 +1,45 @@
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:51:39 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx102) with SMTP; 24 Jul 2010 15:51:39 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 5074095366D
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id DEC1195366F
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id C668A95366D
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id 9EEB53103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id 8367A3103EEE
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Subject: Test 2
+Date: Sat, 24 Jul 2010 15:51:16 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241551.16855.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iL/6eeEA7/vGXc8Az2zQ5lkNsVrprUw5ljr293uKsrVdFBp1i
+ k5eF2TKdovp4qtfEvYkwJlTyCJPnhg0xAPWnM7rhY/+LZUrte1UAr5224vU33NgWA8bmceIWcEOY
+ UsgKg==V1;
+Status: R
+X-Status: N
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+Body of Test 2
diff --git a/resources/mixedmaildir/autotests/data/maildir/new/.keep b/resources/mixedmaildir/autotests/data/maildir/new/.keep
new file mode 100644 (file)
index 0000000..2f1d07a
--- /dev/null
@@ -0,0 +1 @@
+force git to include this file
diff --git a/resources/mixedmaildir/autotests/data/maildir/tmp/.keep b/resources/mixedmaildir/autotests/data/maildir/tmp/.keep
new file mode 100644 (file)
index 0000000..2f1d07a
--- /dev/null
@@ -0,0 +1 @@
+force git to include this file
diff --git a/resources/mixedmaildir/autotests/data/mbox b/resources/mixedmaildir/autotests/data/mbox
new file mode 100644 (file)
index 0000000..b016d7b
--- /dev/null
@@ -0,0 +1,187 @@
+From kevin.krammer@demo.kolab.org Sat Jul 24 15:51:16 2010
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:51:39 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx102) with SMTP; 24 Jul 2010 15:51:39 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 5074095366D
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id DEC1195366F
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id C668A95366D
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id 9EEB53103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id 8367A3103EEE
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Subject: Test 2
+Date: Sat, 24 Jul 2010 15:51:16 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241551.16855.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iL/6eeEA7/vGXc8Az2zQ5lkNsVrprUw5ljr293uKsrVdFBp1i
+ k5eF2TKdovp4qtfEvYkwJlTyCJPnhg0xAPWnM7rhY/+LZUrte1UAr5224vU33NgWA8bmceIWcEOY
+ UsgKg==V1;
+Status: RO
+X-Status: U
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+Body of Test 2
+
+From kevin.krammer@demo.kolab.org Sat Jul 24 15:52:00 2010
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:52:14 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx036) with SMTP; 24 Jul 2010 15:52:14 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 3536595366D
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:14 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 101AC95366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:14 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id ED25794D9DA
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:13 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id C2DA93103EEB
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:13 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id AE2D13103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:13 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Date: Sat, 24 Jul 2010 15:52:00 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241552.01232.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iM2SSXdnXxuLgAcAvPC3Xr5Z6OPcGoHnWX0YgscMEgMNa/UP6
+ 0PsZjBktxR5hqB4N3jaDTD+60EUFg8bPz1GGvD1YActDYJympMTApbcs85zthOivOU3SH3UvpiHN
+ TRN8A==V1;
+Status: RO
+X-Status: RK
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+Body of Test 4
+
+From kevin.krammer@demo.kolab.org Sat Jul 24 15:51:36 2010
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:51:39 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx106) with SMTP; 24 Jul 2010 15:51:39 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id A6DEC95366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 6305495366F
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id 4D0C995366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id 21F263103EEB
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id 06BF13103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Subject: Test 3
+Date: Sat, 24 Jul 2010 15:51:36 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241551.37547.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iL/6eeEA7/vGXc8Az2zQ5lkNsVrprUw5ljr293uKsrVdFBp1i
+ k5eF2TKdovp4qtfEvYkwJlTyCJPnhg0xAPWnM7rhY/+LZUrte1UAr5224vU33NgWA8bmceIWcEOY
+ UsgKg==V1;
+Status: R
+X-Status: N
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+
+From kevin.krammer@demo.kolab.org Sat Jul 24 15:50:45 2010
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:51:41 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx033) with SMTP; 24 Jul 2010 15:51:41 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id BD28395366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:40 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id CA62095366E
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id B5E0195366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id 754B03103EEB
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id 545963103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Subject: Test 1
+Date: Sat, 24 Jul 2010 15:50:45 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241550.46907.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iL/6eeEA7/vGXc8Az2zQ5lkNsVrprUw5ljr293uKsrVdFBp1i
+ k5eF2TKdovp4qtfEvYkwJlTyCJPnhg0xAPWnM7rhY/+LZUrte1UAr5224vU33NgWA8bmceIWcEOY
+ UsgKg==V1;
+Status: RO
+X-Status: RG
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+Body
+of
+Test 1
diff --git a/resources/mixedmaildir/autotests/data/mbox-tagged b/resources/mixedmaildir/autotests/data/mbox-tagged
new file mode 100644 (file)
index 0000000..b016d7b
--- /dev/null
@@ -0,0 +1,187 @@
+From kevin.krammer@demo.kolab.org Sat Jul 24 15:51:16 2010
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:51:39 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx102) with SMTP; 24 Jul 2010 15:51:39 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 5074095366D
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id DEC1195366F
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id C668A95366D
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id 9EEB53103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id 8367A3103EEE
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Subject: Test 2
+Date: Sat, 24 Jul 2010 15:51:16 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241551.16855.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iL/6eeEA7/vGXc8Az2zQ5lkNsVrprUw5ljr293uKsrVdFBp1i
+ k5eF2TKdovp4qtfEvYkwJlTyCJPnhg0xAPWnM7rhY/+LZUrte1UAr5224vU33NgWA8bmceIWcEOY
+ UsgKg==V1;
+Status: RO
+X-Status: U
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+Body of Test 2
+
+From kevin.krammer@demo.kolab.org Sat Jul 24 15:52:00 2010
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:52:14 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx036) with SMTP; 24 Jul 2010 15:52:14 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 3536595366D
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:14 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 101AC95366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:14 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id ED25794D9DA
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:13 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id C2DA93103EEB
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:13 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id AE2D13103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:13 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Date: Sat, 24 Jul 2010 15:52:00 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241552.01232.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iM2SSXdnXxuLgAcAvPC3Xr5Z6OPcGoHnWX0YgscMEgMNa/UP6
+ 0PsZjBktxR5hqB4N3jaDTD+60EUFg8bPz1GGvD1YActDYJympMTApbcs85zthOivOU3SH3UvpiHN
+ TRN8A==V1;
+Status: RO
+X-Status: RK
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+Body of Test 4
+
+From kevin.krammer@demo.kolab.org Sat Jul 24 15:51:36 2010
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:51:39 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx106) with SMTP; 24 Jul 2010 15:51:39 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id A6DEC95366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 6305495366F
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id 4D0C995366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id 21F263103EEB
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id 06BF13103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Subject: Test 3
+Date: Sat, 24 Jul 2010 15:51:36 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241551.37547.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iL/6eeEA7/vGXc8Az2zQ5lkNsVrprUw5ljr293uKsrVdFBp1i
+ k5eF2TKdovp4qtfEvYkwJlTyCJPnhg0xAPWnM7rhY/+LZUrte1UAr5224vU33NgWA8bmceIWcEOY
+ UsgKg==V1;
+Status: R
+X-Status: N
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+
+From kevin.krammer@demo.kolab.org Sat Jul 24 15:50:45 2010
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:51:41 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx033) with SMTP; 24 Jul 2010 15:51:41 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id BD28395366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:40 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id CA62095366E
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id B5E0195366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id 754B03103EEB
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id 545963103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Subject: Test 1
+Date: Sat, 24 Jul 2010 15:50:45 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241550.46907.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iL/6eeEA7/vGXc8Az2zQ5lkNsVrprUw5ljr293uKsrVdFBp1i
+ k5eF2TKdovp4qtfEvYkwJlTyCJPnhg0xAPWnM7rhY/+LZUrte1UAr5224vU33NgWA8bmceIWcEOY
+ UsgKg==V1;
+Status: RO
+X-Status: RG
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+Body
+of
+Test 1
diff --git a/resources/mixedmaildir/autotests/data/mbox-unpurged b/resources/mixedmaildir/autotests/data/mbox-unpurged
new file mode 100644 (file)
index 0000000..b016d7b
--- /dev/null
@@ -0,0 +1,187 @@
+From kevin.krammer@demo.kolab.org Sat Jul 24 15:51:16 2010
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:51:39 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx102) with SMTP; 24 Jul 2010 15:51:39 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 5074095366D
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id DEC1195366F
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id C668A95366D
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id 9EEB53103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id 8367A3103EEE
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Subject: Test 2
+Date: Sat, 24 Jul 2010 15:51:16 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241551.16855.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iL/6eeEA7/vGXc8Az2zQ5lkNsVrprUw5ljr293uKsrVdFBp1i
+ k5eF2TKdovp4qtfEvYkwJlTyCJPnhg0xAPWnM7rhY/+LZUrte1UAr5224vU33NgWA8bmceIWcEOY
+ UsgKg==V1;
+Status: RO
+X-Status: U
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+Body of Test 2
+
+From kevin.krammer@demo.kolab.org Sat Jul 24 15:52:00 2010
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:52:14 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx036) with SMTP; 24 Jul 2010 15:52:14 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 3536595366D
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:14 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 101AC95366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:14 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id ED25794D9DA
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:13 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id C2DA93103EEB
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:13 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id AE2D13103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:52:13 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Date: Sat, 24 Jul 2010 15:52:00 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241552.01232.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iM2SSXdnXxuLgAcAvPC3Xr5Z6OPcGoHnWX0YgscMEgMNa/UP6
+ 0PsZjBktxR5hqB4N3jaDTD+60EUFg8bPz1GGvD1YActDYJympMTApbcs85zthOivOU3SH3UvpiHN
+ TRN8A==V1;
+Status: RO
+X-Status: RK
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+Body of Test 4
+
+From kevin.krammer@demo.kolab.org Sat Jul 24 15:51:36 2010
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:51:39 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx106) with SMTP; 24 Jul 2010 15:51:39 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id A6DEC95366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id 6305495366F
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id 4D0C995366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id 21F263103EEB
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id 06BF13103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:39 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Subject: Test 3
+Date: Sat, 24 Jul 2010 15:51:36 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241551.37547.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iL/6eeEA7/vGXc8Az2zQ5lkNsVrprUw5ljr293uKsrVdFBp1i
+ k5eF2TKdovp4qtfEvYkwJlTyCJPnhg0xAPWnM7rhY/+LZUrte1UAr5224vU33NgWA8bmceIWcEOY
+ UsgKg==V1;
+Status: R
+X-Status: N
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+
+From kevin.krammer@demo.kolab.org Sat Jul 24 15:50:45 2010
+Return-Path: <kevin.krammer@demo.kolab.org>
+Delivered-To: GMX delivery to kevin.krammer@gmx.at
+Received: (qmail invoked by alias); 24 Jul 2010 13:51:41 -0000
+Received: from aktaia.intevation.org (EHLO kolab.intevation.de) [212.95.126.10]
+  by mx0.gmx.net (mx033) with SMTP; 24 Jul 2010 15:51:41 +0200
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id BD28395366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:40 +0200 (CEST)
+X-Virus-Scanned: by amavisd-new at intevation.de
+Received: from localhost (localhost.localdomain [127.0.0.1])
+       by kolab.intevation.de (Postfix) with ESMTP id CA62095366E
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from demo.kolab.org (demo.kolab.org [78.47.168.37])
+       by kolab.intevation.de (Postfix) with ESMTP id B5E0195366A
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from localhost (localhost [127.0.0.1])
+       by demo.kolab.org (Postfix) with ESMTP id 754B03103EEB
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+Received: from artemis.localnet (bergfex.jusinger.at [213.47.44.9])
+       (Authenticated sender: kevin.krammer@demo.kolab.org)
+       by demo.kolab.org (Postfix) with ESMTP id 545963103EE6
+       for <kevin.krammer@gmx.at>; Sat, 24 Jul 2010 15:51:38 +0200 (CEST)
+From: Kevin Krammer <kevin.krammer@demo.kolab.org>
+Organization: Kolab Demo
+To: kevin.krammer@gmx.at
+Subject: Test 1
+Date: Sat, 24 Jul 2010 15:50:45 +0200
+User-Agent: KMail/1.13.3 (Linux/2.6.33-grml; KDE/4.4.4; i686; ; )
+MIME-Version: 1.0
+Content-Type: Text/Plain;
+  charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Message-Id: <201007241550.46907.kevin.krammer@demo.kolab.org>
+X-GMX-Antivirus: 0 (no virus found)
+X-GMX-Antispam: 0 (Mail was not recognized as spam);
+ Detail=5D7Q89H36p5mOUCHqrP9iL/6eeEA7/vGXc8Az2zQ5lkNsVrprUw5ljr293uKsrVdFBp1i
+ k5eF2TKdovp4qtfEvYkwJlTyCJPnhg0xAPWnM7rhY/+LZUrte1UAr5224vU33NgWA8bmceIWcEOY
+ UsgKg==V1;
+Status: RO
+X-Status: RG
+X-KMail-EncryptionState:  
+X-KMail-SignatureState:  
+X-KMail-MDN-Sent:  
+
+Body
+of
+Test 1
diff --git a/resources/mixedmaildir/autotests/itemcreatetest.cpp b/resources/mixedmaildir/autotests/itemcreatetest.cpp
new file mode 100644 (file)
index 0000000..5797bc5
--- /dev/null
@@ -0,0 +1,538 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "mixedmaildirstore.h"
+
+#include "testdatautil.h"
+
+#include "filestore/itemcreatejob.h"
+#include "filestore/itemfetchjob.h"
+
+#include "libmaildir/maildir.h"
+
+#include <kmbox/mbox.h>
+#include <kmime/kmime_message.h>
+
+#include <KRandom>
+#include <QTemporaryDir>
+
+#include <qtest.h>
+#include <QFileInfo>
+#include <QDir>
+
+using namespace Akonadi;
+
+class ItemCreateTest : public QObject
+{
+    Q_OBJECT
+
+public:
+    ItemCreateTest() : QObject(), mStore(0), mDir(0) {}
+
+    ~ItemCreateTest()
+    {
+        delete mStore;
+        delete mDir;
+    }
+
+private:
+    MixedMaildirStore *mStore;
+    QTemporaryDir *mDir;
+
+private Q_SLOTS:
+    void init();
+    void cleanup();
+    void testExpectedFail();
+    void testMBox();
+    void testMaildir();
+};
+
+void ItemCreateTest::init()
+{
+    mStore = new MixedMaildirStore;
+
+    mDir = new QTemporaryDir;
+    QVERIFY(mDir->isValid());
+    QVERIFY(QDir(mDir->path()).exists());
+}
+
+void ItemCreateTest::cleanup()
+{
+    delete mStore;
+    mStore = 0;
+    delete mDir;
+    mDir = 0;
+}
+
+void ItemCreateTest::testExpectedFail()
+{
+    QDir topDir(mDir->path());
+
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("maildir"), topDir.path(), QStringLiteral("data")));
+    QDir dataDir = topDir;
+    QVERIFY(dataDir.cd(QLatin1String("data")));
+    KPIM::Maildir dataMd(dataDir.path(), false);
+    QVERIFY(dataMd.isValid());
+
+    const QStringList dataEntryList = dataMd.entryList();
+    QCOMPARE(dataEntryList.count(), 4);
+    KMime::Message::Ptr msgPtr(new KMime::Message);
+    msgPtr->setContent(KMime::CRLFtoLF(dataMd.readEntry(dataEntryList.first())));
+
+    QVERIFY(topDir.mkdir(QLatin1String("store")));
+    QVERIFY(topDir.cd(QLatin1String("store")));
+    mStore->setPath(topDir.path());
+
+    FileStore::ItemCreateJob *job = 0;
+
+    // test failure of adding item to top level collection
+    Item item;
+    item.setMimeType(KMime::Message::mimeType());
+    item.setPayload<KMime::Message::Ptr>(msgPtr);
+
+    job = mStore->createItem(item, mStore->topLevelCollection());
+
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+
+    // test failure of adding item to non existent collection
+    Collection collection;
+    collection.setName(QStringLiteral("collection"));
+    collection.setRemoteId(QStringLiteral("collection"));
+    collection.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->createItem(item, collection);
+
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+}
+
+void ItemCreateTest::testMBox()
+{
+    QDir topDir(mDir->path());
+
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("maildir"), topDir.path(), QStringLiteral("data")));
+    QDir dataDir = topDir;
+    QVERIFY(dataDir.cd(QLatin1String("data")));
+    KPIM::Maildir dataMd(dataDir.path(), false);
+    QVERIFY(dataMd.isValid());
+
+    const QStringList dataEntryList = dataMd.entryList();
+    QCOMPARE(dataEntryList.count(), 4);
+    KMime::Message::Ptr msgPtr1(new KMime::Message);
+    msgPtr1->setContent(KMime::CRLFtoLF(dataMd.readEntry(dataEntryList.first())));
+    KMime::Message::Ptr msgPtr2(new KMime::Message);
+    msgPtr2->setContent(KMime::CRLFtoLF(dataMd.readEntry(dataEntryList.last())));
+
+    QVERIFY(topDir.mkdir(QLatin1String("store")));
+    QVERIFY(topDir.cd(QLatin1String("store")));
+
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox"), topDir.path(), QStringLiteral("collection1")));
+
+    QFileInfo fileInfo1(topDir.path(), QStringLiteral("collection1"));
+    KMBox::MBox mbox1;
+    QVERIFY(mbox1.load(fileInfo1.absoluteFilePath()));
+    QCOMPARE((int)mbox1.entries().count(), 4);
+    const int size1 = fileInfo1.size();
+
+    // simulate empty mbox
+    QFileInfo fileInfo2(topDir.path(), QStringLiteral("collection2"));
+    QFile file2(fileInfo2.absoluteFilePath());
+    QVERIFY(file2.open(QIODevice::WriteOnly));
+    file2.close();
+    QVERIFY(file2.exists());
+    QCOMPARE((int)file2.size(), 0);
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    const QVariant colListVar = QVariant::fromValue<Collection::List>(Collection::List());
+    QVariant var;
+    Collection::List collections;
+    Item::List items;
+    QMap<QByteArray, int> flagCounts;
+
+    FileStore::ItemCreateJob *job = 0;
+    FileStore::ItemFetchJob *itemFetch = 0;
+
+    // test adding to empty mbox
+    Collection collection2;
+    collection2.setName(QStringLiteral("collection2"));
+    collection2.setRemoteId(QStringLiteral("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+
+    Item item1;
+    item1.setId(KRandom::random());
+    item1.setMimeType(KMime::Message::mimeType());
+    item1.setPayload<KMime::Message::Ptr>(msgPtr1);
+
+    job = mStore->createItem(item1, collection2);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    Item item = job->item();
+    QCOMPARE(item.id(), item1.id());
+    QVERIFY(!item.remoteId().isEmpty());
+    QCOMPARE(item.remoteId(), QStringLiteral("0"));
+    QCOMPARE(item.parentCollection(), collection2);
+
+    fileInfo2.refresh();
+    QVERIFY(fileInfo2.size() > 0);
+    const int size2 = fileInfo2.size();
+
+    KMBox::MBox mbox2;
+    QVERIFY(mbox2.load(fileInfo2.absoluteFilePath()));
+    QCOMPARE((int)mbox2.entries().count(), 1);
+
+    Item item2;
+    item2.setId(KRandom::random());
+    item2.setMimeType(KMime::Message::mimeType());
+    item2.setPayload<KMime::Message::Ptr>(msgPtr2);
+
+    job = mStore->createItem(item2, collection2);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    item = job->item();
+    QCOMPARE(item.id(), item2.id());
+    QVERIFY(!item.remoteId().isEmpty());
+    QCOMPARE(item.remoteId(), QString::number(size2 + 1));
+    QCOMPARE(item.parentCollection(), collection2);
+
+    fileInfo2.refresh();
+    QVERIFY(fileInfo2.size() > 0);
+
+    QVERIFY(mbox2.load(fileInfo2.absoluteFilePath()));
+    QCOMPARE((int)mbox2.entries().count(), 2);
+
+    // test adding to non-empty mbox
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->createItem(item1, collection1);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    item = job->item();
+    QCOMPARE(item.id(), item1.id());
+    QVERIFY(!item.remoteId().isEmpty());
+    QCOMPARE(item.remoteId(), QString::number(size1 + 1));
+    QCOMPARE(item.parentCollection(), collection1);
+
+    fileInfo1.refresh();
+    QVERIFY(fileInfo1.size() > size1);
+
+    QVERIFY(mbox1.load(fileInfo1.absoluteFilePath()));
+    QCOMPARE((int)mbox1.entries().count(), 5);
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection1);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection1);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 5);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    job = mStore->createItem(item2, collection1);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    item = job->item();
+    QCOMPARE(item.id(), item2.id());
+    QVERIFY(!item.remoteId().isEmpty());
+    QCOMPARE(item.remoteId(), QString::number(size1 + 1 + size2 + 1));
+    QCOMPARE(item.parentCollection(), collection1);
+
+    fileInfo1.refresh();
+    QVERIFY(fileInfo1.size() > (size1 + size2));
+
+    QVERIFY(mbox1.load(fileInfo1.absoluteFilePath()));
+    QCOMPARE((int)mbox1.entries().count(), 6);
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection1);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection1);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 6);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+}
+
+void ItemCreateTest::testMaildir()
+{
+    QDir topDir(mDir->path());
+
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("maildir"), topDir.path(), QStringLiteral("data")));
+    QDir dataDir = topDir;
+    QVERIFY(dataDir.cd(QLatin1String("data")));
+    KPIM::Maildir dataMd(dataDir.path(), false);
+    QVERIFY(dataMd.isValid());
+
+    const QStringList dataEntryList = dataMd.entryList();
+    QCOMPARE(dataEntryList.count(), 4);
+    KMime::Message::Ptr msgPtr1(new KMime::Message);
+    msgPtr1->setContent(KMime::CRLFtoLF(dataMd.readEntry(dataEntryList.first())));
+    KMime::Message::Ptr msgPtr2(new KMime::Message);
+    msgPtr2->setContent(KMime::CRLFtoLF(dataMd.readEntry(dataEntryList.last())));
+
+    QVERIFY(topDir.mkdir(QLatin1String("store")));
+    QVERIFY(topDir.cd(QLatin1String("store")));
+
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("maildir"), topDir.path(), QStringLiteral("collection1")));
+
+    KPIM::Maildir topLevelMd(topDir.path(), true);
+    KPIM::Maildir md1 = topLevelMd.subFolder(QStringLiteral("collection1"));
+    QVERIFY(md1.isValid());
+
+    QSet<QString> entrySet1 = QSet<QString>::fromList(md1.entryList());
+    QCOMPARE((int)entrySet1.count(), 4);
+
+    // simulate empty maildir
+    KPIM::Maildir md2(topLevelMd.addSubFolder(QStringLiteral("collection2")), false);
+    QVERIFY(md2.isValid());
+
+    QSet<QString> entrySet2 = QSet<QString>::fromList(md2.entryList());
+    QCOMPARE((int)entrySet2.count(), 0);
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    const QVariant colListVar = QVariant::fromValue<Collection::List>(Collection::List());
+    QVariant var;
+    Collection::List collections;
+    Item::List items;
+    QMap<QByteArray, int> flagCounts;
+
+    QSet<QString> entrySet;
+    QSet<QString> newIdSet;
+    QString newId;
+
+    FileStore::ItemCreateJob *job = 0;
+    FileStore::ItemFetchJob *itemFetch = 0;
+
+    // test adding to empty maildir
+    Collection collection2;
+    collection2.setName(QStringLiteral("collection2"));
+    collection2.setRemoteId(QStringLiteral("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+
+    Item item1;
+    item1.setId(KRandom::random());
+    item1.setMimeType(KMime::Message::mimeType());
+    item1.setPayload<KMime::Message::Ptr>(msgPtr1);
+
+    job = mStore->createItem(item1, collection2);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    Item item = job->item();
+    QCOMPARE(item.id(), item1.id());
+    QVERIFY(!item.remoteId().isEmpty());
+    QCOMPARE(item.parentCollection(), collection2);
+
+    entrySet = QSet<QString>::fromList(md2.entryList());
+    QCOMPARE((int)entrySet.count(), 1);
+
+    newIdSet = entrySet.subtract(entrySet2);
+    QCOMPARE((int)newIdSet.count(), 1);
+
+    newId = *newIdSet.cbegin();
+    QCOMPARE(item.remoteId(), newId);
+    entrySet2 << newId;
+    QCOMPARE((int)entrySet2.count(), 1);
+
+    Item item2;
+    item2.setId(KRandom::random());
+    item2.setMimeType(KMime::Message::mimeType());
+    item2.setPayload<KMime::Message::Ptr>(msgPtr2);
+
+    job = mStore->createItem(item2, collection2);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    item = job->item();
+    QCOMPARE(item.id(), item2.id());
+    QVERIFY(!item.remoteId().isEmpty());
+    QCOMPARE(item.parentCollection(), collection2);
+
+    entrySet = QSet<QString>::fromList(md2.entryList());
+    QCOMPARE((int)entrySet.count(), 2);
+
+    newIdSet = entrySet.subtract(entrySet2);
+    QCOMPARE((int)newIdSet.count(), 1);
+
+    newId = *newIdSet.cbegin();
+    QCOMPARE(item.remoteId(), newId);
+    entrySet2 << newId;
+    QCOMPARE((int)entrySet2.count(), 2);
+
+    // test adding to non-empty maildir
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->createItem(item1, collection1);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    item = job->item();
+    QCOMPARE(item.id(), item1.id());
+    QVERIFY(!item.remoteId().isEmpty());
+    QCOMPARE(item.parentCollection(), collection1);
+
+    entrySet = QSet<QString>::fromList(md1.entryList());
+    QCOMPARE((int)entrySet.count(), 5);
+
+    newIdSet = entrySet.subtract(entrySet1);
+    QCOMPARE((int)newIdSet.count(), 1);
+
+    newId = *newIdSet.cbegin();
+    QCOMPARE(item.remoteId(), newId);
+    entrySet1 << newId;
+    QCOMPARE((int)entrySet1.count(), 5);
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection1);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection1);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 5);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    job = mStore->createItem(item2, collection1);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    item = job->item();
+    QCOMPARE(item.id(), item2.id());
+    QVERIFY(!item.remoteId().isEmpty());
+    QCOMPARE(item.parentCollection(), collection1);
+
+    entrySet = QSet<QString>::fromList(md1.entryList());
+    QCOMPARE((int)entrySet.count(), 6);
+
+    newIdSet = entrySet.subtract(entrySet1);
+    QCOMPARE((int)newIdSet.count(), 1);
+
+    newId = *newIdSet.cbegin();
+    QCOMPARE(item.remoteId(), newId);
+    entrySet1 << newId;
+    QCOMPARE((int)entrySet1.count(), 6);
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection1);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection1);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 6);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+}
+
+QTEST_MAIN(ItemCreateTest)
+
+#include "itemcreatetest.moc"
+
diff --git a/resources/mixedmaildir/autotests/itemdeletetest.cpp b/resources/mixedmaildir/autotests/itemdeletetest.cpp
new file mode 100644 (file)
index 0000000..7553a8e
--- /dev/null
@@ -0,0 +1,609 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "mixedmaildirstore.h"
+
+#include "testdatautil.h"
+
+#include "filestore/entitycompactchangeattribute.h"
+#include "filestore/itemdeletejob.h"
+#include "filestore/itemfetchjob.h"
+#include "filestore/storecompactjob.h"
+
+#include "libmaildir/maildir.h"
+
+#include <kmbox/mbox.h>
+#include <kmime/kmime_message.h>
+
+#include <KRandom>
+#include <QTemporaryDir>
+
+#include <QSignalSpy>
+
+#include <qtest.h>
+#include <QFileInfo>
+#include <QDir>
+
+using namespace Akonadi;
+
+static Collection::List collectionsFromSpy(QSignalSpy *spy)
+{
+    Collection::List collections;
+
+    QListIterator<QList<QVariant> > it(*spy);
+    while (it.hasNext()) {
+        const QList<QVariant> invocation = it.next();
+        Q_ASSERT(invocation.count() == 1);
+
+        collections << invocation.first().value<Collection::List>();
+    }
+
+    return collections;
+}
+
+static Item::List itemsFromSpy(QSignalSpy *spy)
+{
+    Item::List items;
+
+    QListIterator<QList<QVariant> > it(*spy);
+    while (it.hasNext()) {
+        const QList<QVariant> invocation = it.next();
+        Q_ASSERT(invocation.count() == 1);
+
+        items << invocation.first().value<Item::List>();
+    }
+
+    return items;
+}
+
+static bool fullEntryCompare(const KMBox::MBoxEntry &a, const KMBox::MBoxEntry &b)
+{
+    return a.messageOffset() == b.messageOffset() &&
+           a.separatorSize() == b.separatorSize() &&
+           a.messageSize() == b.messageSize();
+}
+
+class ItemDeleteTest : public QObject
+{
+    Q_OBJECT
+
+public:
+    ItemDeleteTest() : QObject(), mStore(0), mDir(0)
+    {
+        // for monitoring signals
+        qRegisterMetaType<Akonadi::Collection::List>();
+        qRegisterMetaType<Akonadi::Item::List>();
+    }
+
+    ~ItemDeleteTest()
+    {
+        delete mStore;
+        delete mDir;
+    }
+
+private:
+    MixedMaildirStore *mStore;
+    QTemporaryDir *mDir;
+
+private Q_SLOTS:
+    void init();
+    void cleanup();
+    void testMaildir();
+    void testMBox();
+    void testCachePreservation();
+    void testExpectedFailure();
+};
+
+void ItemDeleteTest::init()
+{
+    mStore = new MixedMaildirStore;
+
+    mDir = new QTemporaryDir;
+    QVERIFY(mDir->isValid());
+    QVERIFY(QDir(mDir->path()).exists());
+}
+
+void ItemDeleteTest::cleanup()
+{
+    delete mStore;
+    mStore = 0;
+    delete mDir;
+    mDir = 0;
+}
+
+void ItemDeleteTest::testMaildir()
+{
+    QDir topDir(mDir->path());
+
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), topDir.path(), QStringLiteral("collection1")));
+
+    KPIM::Maildir topLevelMd(topDir.path(), true);
+    KPIM::Maildir md1 = topLevelMd.subFolder(QStringLiteral("collection1"));
+    QVERIFY(md1.isValid());
+
+    QSet<QString> entrySet1 = QSet<QString>::fromList(md1.entryList());
+    QCOMPARE((int)entrySet1.count(), 4);
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    FileStore::ItemDeleteJob *job = 0;
+    QSet<QString> entrySet;
+    QSet<QString> delIdSet;
+    QString delId;
+
+    // test deleting one message
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    Item item1;
+    item1.setMimeType(KMime::Message::mimeType());
+    item1.setId(KRandom::random());
+    item1.setRemoteId(*entrySet1.cbegin());
+    item1.setParentCollection(collection1);
+
+    job = mStore->deleteItem(item1);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    Item item = job->item();
+    QCOMPARE(item.id(), item1.id());
+
+    entrySet = QSet<QString>::fromList(md1.entryList());
+    QCOMPARE((int)entrySet.count(), 3);
+
+    delIdSet = entrySet1.subtract(entrySet);
+    QCOMPARE((int)delIdSet.count(), 1);
+
+    delId = *delIdSet.cbegin();
+    QCOMPARE(delId, *entrySet1.cbegin());
+    QCOMPARE(delId, item.remoteId());
+
+    // test failure of deleting again
+    job = mStore->deleteItem(item1);
+
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+}
+
+void ItemDeleteTest::testMBox()
+{
+    QDir topDir(mDir->path());
+
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), topDir.path(), QStringLiteral("collection1")));
+
+    QFileInfo fileInfo1(topDir.path(), QStringLiteral("collection1"));
+    KMBox::MBox mbox1;
+    QVERIFY(mbox1.load(fileInfo1.absoluteFilePath()));
+    KMBox::MBoxEntry::List entryList1 = mbox1.entries();
+    QCOMPARE((int)entryList1.count(), 4);
+    int size1 = fileInfo1.size();
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    FileStore::ItemDeleteJob *job = 0;
+    FileStore::ItemFetchJob *itemFetch = 0;
+    FileStore::StoreCompactJob *storeCompact = 0;
+
+    Item::List items;
+    Collection::List collections;
+    KMBox::MBoxEntry::List entryList;
+
+    QSignalSpy *collectionsSpy = 0;
+    QSignalSpy *itemsSpy = 0;
+
+    QVariant var;
+
+    // test deleting last item in mbox
+    // file stays untouched, message still accessible through MBox, but item gone
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    Item item4;
+    item4.setMimeType(KMime::Message::mimeType());
+    item4.setId(KRandom::random());
+    item4.setRemoteId(QString::number(entryList1.value(3).messageOffset()));
+    item4.setParentCollection(collection1);
+
+    job = mStore->deleteItem(item4);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    Item item = job->item();
+    QCOMPARE(item.id(), item4.id());
+
+    fileInfo1.refresh();
+    QCOMPARE((int) fileInfo1.size(), size1);
+    QVERIFY(mbox1.load(fileInfo1.absoluteFilePath()));
+    entryList = mbox1.entries();
+    QCOMPARE(entryList.count(), entryList1.count());
+    QCOMPARE(entryList.value(3).messageOffset(), entryList1.value(3).messageOffset());
+
+    var = job->property("compactStore");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.type(), QVariant::Bool);
+    QCOMPARE(var.toBool(), true);
+
+    itemFetch = mStore->fetchItems(collection1);
+
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 3);
+    QCOMPARE(items.value(0).remoteId(), QString::number(entryList1.value(0).messageOffset()));
+    QCOMPARE(items.value(1).remoteId(), QString::number(entryList1.value(1).messageOffset()));
+    QCOMPARE(items.value(2).remoteId(), QString::number(entryList1.value(2).messageOffset()));
+
+    // test that the item is purged from the file on store compaction
+    // last item purging does not change any others
+    storeCompact = mStore->compactStore();
+
+    collectionsSpy = new QSignalSpy(storeCompact, SIGNAL(collectionsChanged(Akonadi::Collection::List)));
+    itemsSpy = new QSignalSpy(storeCompact, SIGNAL(itemsChanged(Akonadi::Item::List)));
+
+    QVERIFY(storeCompact->exec());
+    QCOMPARE(storeCompact->error(), 0);
+
+    collections = storeCompact->changedCollections();
+    QCOMPARE(collections.count(), 0);
+    items = storeCompact->changedItems();
+    QCOMPARE(items.count(), 0);
+
+    QCOMPARE(collectionsFromSpy(collectionsSpy), collections);
+    QCOMPARE(itemsFromSpy(itemsSpy), items);
+
+    fileInfo1.refresh();
+    QVERIFY(fileInfo1.size() < size1);
+    size1 = fileInfo1.size();
+    QVERIFY(mbox1.load(fileInfo1.absoluteFilePath()));
+    entryList = mbox1.entries();
+    entryList1.pop_back();
+    QVERIFY(std::equal(entryList1.begin(), entryList1.end(), entryList.begin(), fullEntryCompare));
+
+    // test deleting item somewhere between first and last
+    // again, file stays untouched, message still accessible through MBox, but item gone
+    Item item2;
+    item2.setMimeType(KMime::Message::mimeType());
+    item2.setId(KRandom::random());
+    item2.setRemoteId(QString::number(entryList1.value(1).messageOffset()));
+    item2.setParentCollection(collection1);
+
+    job = mStore->deleteItem(item2);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    item = job->item();
+    QCOMPARE(item.id(), item2.id());
+
+    fileInfo1.refresh();
+    QCOMPARE((int) fileInfo1.size(), size1);
+    QVERIFY(mbox1.load(fileInfo1.absoluteFilePath()));
+    entryList = mbox1.entries();
+    QCOMPARE(entryList.count(), entryList1.count());
+    QCOMPARE(entryList.value(1).messageOffset(), entryList1.value(1).messageOffset());
+
+    var = job->property("compactStore");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.type(), QVariant::Bool);
+    QCOMPARE(var.toBool(), true);
+
+    itemFetch = mStore->fetchItems(collection1);
+
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 2);
+    QCOMPARE(items.value(0).remoteId(), QString::number(entryList1.value(0).messageOffset()));
+    QCOMPARE(items.value(1).remoteId(), QString::number(entryList1.value(2).messageOffset()));
+
+    // test that the item is purged from the file on store compaction
+    // non-last item purging changes all items after it
+    storeCompact = mStore->compactStore();
+
+    collectionsSpy = new QSignalSpy(storeCompact, SIGNAL(collectionsChanged(Akonadi::Collection::List)));
+    itemsSpy = new QSignalSpy(storeCompact, SIGNAL(itemsChanged(Akonadi::Item::List)));
+
+    QVERIFY(storeCompact->exec());
+    QCOMPARE(storeCompact->error(), 0);
+
+    collections = storeCompact->changedCollections();
+    QCOMPARE(collections.count(), 1);
+    items = storeCompact->changedItems();
+    QCOMPARE(items.count(), 1);
+
+    QCOMPARE(collectionsFromSpy(collectionsSpy), collections);
+    QCOMPARE(itemsFromSpy(itemsSpy), items);
+
+    Item item3;
+    item3.setRemoteId(QString::number(entryList1.value(2).messageOffset()));
+
+    item = items.first();
+    QCOMPARE(item3.remoteId(), item.remoteId());
+
+    QVERIFY(item.hasAttribute<FileStore::EntityCompactChangeAttribute>());
+    FileStore::EntityCompactChangeAttribute *attribute =
+        item.attribute<FileStore::EntityCompactChangeAttribute>();
+
+    QString newRemoteId = attribute->remoteId();
+    QVERIFY(!newRemoteId.isEmpty());
+
+    fileInfo1.refresh();
+    QVERIFY(fileInfo1.size() < size1);
+    size1 = fileInfo1.size();
+    QVERIFY(mbox1.load(fileInfo1.absoluteFilePath()));
+    entryList = mbox1.entries();
+    QCOMPARE(QString::number(entryList.value(1).messageOffset()), newRemoteId);
+
+    entryList1.removeAt(1);
+    QCOMPARE(entryList1.count(), entryList.count());
+    QCOMPARE(QString::number(entryList1.value(1).messageOffset()), item3.remoteId());
+}
+
+void ItemDeleteTest::testCachePreservation()
+{
+    QDir topDir(mDir->path());
+
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), topDir.path(), QStringLiteral("collection1")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), topDir.path(), QStringLiteral("collection2")));
+
+    KPIM::Maildir topLevelMd(topDir.path(), true);
+    KPIM::Maildir md1 = topLevelMd.subFolder(QStringLiteral("collection1"));
+    QVERIFY(md1.isValid());
+
+    QSet<QString> entrySet1 = QSet<QString>::fromList(md1.entryList());
+    QCOMPARE((int)entrySet1.count(), 4);
+
+    QFileInfo fileInfo2(topDir.path(), QStringLiteral("collection2"));
+    KMBox::MBox mbox2;
+    QVERIFY(mbox2.load(fileInfo2.absoluteFilePath()));
+    KMBox::MBoxEntry::List entryList2 = mbox2.entries();
+    QCOMPARE((int)entryList2.count(), 4);
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    const QVariant colListVar = QVariant::fromValue<Collection::List>(Collection::List());
+    QVariant var;
+    Collection::List collections;
+    Item::List items;
+    QMap<QByteArray, int> flagCounts;
+
+    FileStore::ItemDeleteJob *job = 0;
+    FileStore::ItemFetchJob *itemFetch = 0;
+
+    // test deleting from maildir
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    Item item1;
+    item1.setMimeType(KMime::Message::mimeType());
+    item1.setId(KRandom::random());
+    item1.setRemoteId(*entrySet1.cbegin());
+    item1.setParentCollection(collection1);
+
+    job = mStore->deleteItem(item1);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    Item item = job->item();
+    QCOMPARE(item.id(), item1.id());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection1);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection1);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 3);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    // TODO since we don't know which message we've deleted, we can only check if some flags are present
+    int flagCountTotal = 0;
+    Q_FOREACH (int count, flagCounts) {
+        flagCountTotal += count;
+    }
+    QVERIFY(flagCountTotal > 0);
+    flagCounts.clear();
+
+    // test deleting from mbox
+    Collection collection2;
+    collection2.setName(QStringLiteral("collection2"));
+    collection2.setRemoteId(QStringLiteral("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+
+    Item item2;
+    item2.setMimeType(KMime::Message::mimeType());
+    item2.setId(KRandom::random());
+    item2.setRemoteId(QString::number(entryList2.value(1).messageOffset()));
+    item2.setParentCollection(collection2);
+
+    job = mStore->deleteItem(item2);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    item = job->item();
+    QCOMPARE(item.id(), item2.id());
+
+    // at this point no change has been written to disk yet, so index and mbox file are
+    // still in sync
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(!var.isValid());
+
+    FileStore::StoreCompactJob *storeCompact = mStore->compactStore();
+
+    QVERIFY(storeCompact->exec());
+    QCOMPARE(storeCompact->error(), 0);
+
+    // check for index preservation
+    var = storeCompact->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection2);
+
+    // get the items and check the flags (see data/README)
+    itemFetch = mStore->fetchItems(collection2);
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    items = itemFetch->items();
+    QCOMPARE((int)items.count(), 3);
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    // we've deleted message 2, it flagged TODO and seen
+    QCOMPARE(flagCounts[ "\\SEEN" ], 1);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    flagCounts.clear();
+}
+
+void ItemDeleteTest::testExpectedFailure()
+{
+    QDir topDir(mDir->path());
+
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), topDir.path(), QStringLiteral("collection1")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), topDir.path(), QStringLiteral("collection2")));
+
+    KPIM::Maildir topLevelMd(topDir.path(), true);
+    KPIM::Maildir md1 = topLevelMd.subFolder(QStringLiteral("collection1"));
+    QVERIFY(md1.isValid());
+
+    QSet<QString> entrySet1 = QSet<QString>::fromList(md1.entryList());
+    QCOMPARE((int)entrySet1.count(), 4);
+
+    QFileInfo fileInfo2(topDir.path(), QStringLiteral("collection2"));
+    KMBox::MBox mbox2;
+    QVERIFY(mbox2.load(fileInfo2.absoluteFilePath()));
+    KMBox::MBoxEntry::List entryList2 = mbox2.entries();
+    QCOMPARE((int)entryList2.count(), 4);
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    FileStore::ItemDeleteJob *job = 0;
+    FileStore::ItemFetchJob *itemFetch = 0;
+    FileStore::StoreCompactJob *storeCompact = 0;
+
+    // test failure of fetching an item previously deleted from maildir
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    Item item1_1;
+    item1_1.setRemoteId(*entrySet1.cbegin());
+    item1_1.setParentCollection(collection1);
+
+    job = mStore->deleteItem(item1_1);
+
+    QVERIFY(job->exec());
+
+    itemFetch = mStore->fetchItem(item1_1);
+
+    QVERIFY(!itemFetch->exec());
+    QCOMPARE(itemFetch->error(), (int)FileStore::Job::InvalidJobContext);
+
+    // test failure of deleting an item from maildir again
+    job = mStore->deleteItem(item1_1);
+
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+
+    // test failure of fetching an item previously deleted from mbox
+    Collection collection2;
+    collection2.setName(QStringLiteral("collection2"));
+    collection2.setRemoteId(QStringLiteral("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+
+    Item item2_1;
+    item2_1.setRemoteId(QString::number(entryList2.value(0).messageOffset()));
+    item2_1.setParentCollection(collection2);
+
+    job = mStore->deleteItem(item2_1);
+
+    QVERIFY(job->exec());
+
+    itemFetch = mStore->fetchItem(item2_1);
+
+    QVERIFY(!itemFetch->exec());
+    QCOMPARE(itemFetch->error(), (int)FileStore::Job::InvalidJobContext);
+
+    // test failure of deleting an item from mbox again
+    job = mStore->deleteItem(item2_1);
+
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+
+    // compact store and check that offset 0 is a valid remoteId again, but
+    // offset of other items (e.f. item 4) are no longer valid (moved to the front of the file)
+    storeCompact = mStore->compactStore();
+
+    QVERIFY(storeCompact->exec());
+
+    itemFetch = mStore->fetchItem(item2_1);
+
+    QVERIFY(itemFetch->exec());
+
+    Item item4_1;
+    item4_1.setRemoteId(QString::number(entryList2.value(3).messageOffset()));
+    item4_1.setParentCollection(collection2);
+
+    itemFetch = mStore->fetchItem(item4_1);
+
+    QVERIFY(!itemFetch->exec());
+    QCOMPARE(itemFetch->error(), (int)FileStore::Job::InvalidJobContext);
+}
+
+QTEST_MAIN(ItemDeleteTest)
+
+#include "itemdeletetest.moc"
+
diff --git a/resources/mixedmaildir/autotests/itemfetchtest.cpp b/resources/mixedmaildir/autotests/itemfetchtest.cpp
new file mode 100644 (file)
index 0000000..f1ee86b
--- /dev/null
@@ -0,0 +1,1165 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "mixedmaildirstore.h"
+#include "mixedmaildirresource_debug.h"
+#include "testdatautil.h"
+
+#include "filestore/itemcreatejob.h"
+#include "filestore/itemfetchjob.h"
+
+#include "libmaildir/maildir.h"
+
+#include <kmbox/mbox.h>
+#include <KMime/kmime/kmime_message.h>
+
+#include <akonadi/kmime/messageparts.h>
+#include <itemfetchscope.h>
+
+#include <KRandom>
+#include <KRandomSequence>
+#include <QTemporaryDir>
+
+#include <QSignalSpy>
+
+#include <qtest.h>
+#include <QFileInfo>
+#include <QDir>
+using namespace Akonadi;
+using namespace KMBox;
+
+static Item::List itemsFromSpy(QSignalSpy *spy)
+{
+    Item::List items;
+
+    QListIterator<QList<QVariant> > it(*spy);
+    while (it.hasNext()) {
+        const QList<QVariant> invocation = it.next();
+        Q_ASSERT(invocation.count() == 1);
+
+        items << invocation.first().value<Item::List>();
+    }
+
+    return items;
+}
+
+// copied from mail serializer plugin, Copyright (c) 2007 Till Adam <adam@kde.org>
+static QSet<QByteArray> messageParts(const KMime::Message::Ptr &msgPtr)
+{
+    QSet<QByteArray> set;
+    // FIXME: we actually want "has any header" here, but the kmime api doesn't offer that yet
+    if (msgPtr->hasContent() || msgPtr->hasHeader("Message-ID")) {
+        set << MessagePart::Envelope << MessagePart::Header;
+        if (!msgPtr->body().isEmpty() || !msgPtr->contents().isEmpty()) {
+            set << MessagePart::Body;
+        }
+    }
+    return set;
+}
+
+// needed to sort maildir directory entries by filename which is their
+// remoteId. tagListHash.contains tests below need sorting of entries.
+static bool itemLessThanByRemoteId(const Item &item1, const Item &item2)
+{
+    return item1.remoteId() < item2.remoteId();
+}
+
+class ItemFetchTest : public QObject
+{
+    Q_OBJECT
+
+public:
+    ItemFetchTest()
+        : QObject(), mStore(0), mDir(0), mIndexFilePattern(QLatin1String(".%1.index"))
+    {
+        // for monitoring signals
+        qRegisterMetaType<Akonadi::Collection::List>();
+        qRegisterMetaType<Akonadi::Item::List>();
+    }
+
+    ~ItemFetchTest()
+    {
+        delete mStore;
+        delete mDir;
+    }
+
+    QString indexFile(const QString &folder) const
+    {
+        return mIndexFilePattern.arg(folder);
+    }
+
+    QString indexFile(const QFileInfo &folderFileInfo) const
+    {
+        return QFileInfo(folderFileInfo.absolutePath(),
+                         mIndexFilePattern.arg(folderFileInfo.fileName())).absoluteFilePath();
+    }
+
+private:
+    MixedMaildirStore *mStore;
+    QTemporaryDir *mDir;
+
+    const QString mIndexFilePattern;
+
+private Q_SLOTS:
+    void init();
+    void cleanup();
+    void testListingMaildir();
+    void testListingMBox();
+    void testSingleItemFetchMaildir();
+    void testSingleItemFetchMBox();
+};
+
+void ItemFetchTest::init()
+{
+    mStore = new MixedMaildirStore;
+
+    mDir = new QTemporaryDir;
+    QVERIFY(mDir->isValid());
+    QVERIFY(QDir(mDir->path()).exists());
+}
+
+void ItemFetchTest::cleanup()
+{
+    delete mStore;
+    mStore = 0;
+    delete mDir;
+    mDir = 0;
+}
+
+void ItemFetchTest::testListingMaildir()
+{
+    QDir topDir(mDir->path());
+
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("maildir"), topDir.path(), QLatin1String("collection1")));
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("maildir"), topDir.path(), QLatin1String("collection2")));
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("maildir-tagged"), topDir.path(), QLatin1String("collection3")));
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("dimap"), topDir.path(), QLatin1String("collection4")));
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("maildir-tagged"), topDir.path(), QLatin1String("collection5")));
+
+    KPIM::Maildir topLevelMd(topDir.path(), true);
+
+    KPIM::Maildir md1 = topLevelMd.subFolder(QLatin1String("collection1"));
+    QSet<QString> entrySet1 = QSet<QString>::fromList(md1.entryList());
+    QCOMPARE((int)entrySet1.count(), 4);
+
+    QFileInfo indexFileInfo1(indexFile(QFileInfo(md1.path())));
+    QVERIFY(QFile::remove(indexFileInfo1.absoluteFilePath()));
+
+    KPIM::Maildir md2 = topLevelMd.subFolder(QLatin1String("collection2"));
+    QSet<QString> entrySet2 = QSet<QString>::fromList(md2.entryList());
+    QCOMPARE((int)entrySet2.count(), 4);
+
+    KPIM::Maildir md3 = topLevelMd.subFolder(QLatin1String("collection3"));
+    QSet<QString> entrySet3 = QSet<QString>::fromList(md3.entryList());
+    QCOMPARE((int)entrySet3.count(), 4);
+
+    KPIM::Maildir md4 = topLevelMd.subFolder(QLatin1String("collection4"));
+    QSet<QString> entrySet4 = QSet<QString>::fromList(md4.entryList());
+    QCOMPARE((int)entrySet4.count(), 4);
+
+    KPIM::Maildir md5 = topLevelMd.subFolder(QLatin1String("collection5"));
+    QSet<QString> entrySet5 = QSet<QString>::fromList(md5.entryList());
+    QCOMPARE((int)entrySet5.count(), 4);
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    FileStore::ItemFetchJob *job = 0;
+
+    QSignalSpy *spy = 0;
+    Item::List items;
+
+    QHash<QString, QVariant> uidHash;
+    const QVariant varUidHash = QVariant::fromValue< QHash<QString, QVariant> >(uidHash);
+    QHash<QString, QVariant> tagListHash;
+    const QVariant varTagListHash = QVariant::fromValue< QHash<QString, QVariant> >(tagListHash);
+    QVariant var;
+
+    QSet<QString> entrySet;
+    QMap<QByteArray, int> flagCounts;
+
+    // test listing maildir without index
+    Collection collection1;
+    collection1.setName(QLatin1String("collection1"));
+    collection1.setRemoteId(QLatin1String("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->fetchItems(collection1);
+
+    spy = new QSignalSpy(job, SIGNAL(itemsReceived(Akonadi::Item::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    items = job->items();
+    QCOMPARE((int)items.count(), 4);
+    QCOMPARE(itemsFromSpy(spy), items);
+
+    entrySet = entrySet1;
+    QVERIFY(entrySet.remove(items[ 0 ].remoteId()));
+    QVERIFY(entrySet.remove(items[ 1 ].remoteId()));
+    QVERIFY(entrySet.remove(items[ 2 ].remoteId()));
+    QVERIFY(entrySet.remove(items[ 3 ].remoteId()));
+
+    QCOMPARE(items[ 0 ].parentCollection(), collection1);
+    QCOMPARE(items[ 1 ].parentCollection(), collection1);
+    QCOMPARE(items[ 2 ].parentCollection(), collection1);
+    QCOMPARE(items[ 3 ].parentCollection(), collection1);
+
+    QVERIFY(!items[ 0 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 1 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 2 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 3 ].hasPayload<KMime::Message::Ptr>());
+
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    // no flags from maildir file name, no advanced flags without index
+    QCOMPARE(flagCounts.count(), 0);
+    QCOMPARE(flagCounts[ "\\SEEN" ], 0);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 0);
+    QCOMPARE(flagCounts[ "$TODO" ], 0);
+    flagCounts.clear();
+
+    var = job->property("remoteIdToTagList");
+    QVERIFY(!var.isValid());
+
+    // test listing empty mbox without index
+    Q_FOREACH (const QString &entry, entrySet1) {
+        QVERIFY(md1.removeEntry(entry));
+    }
+    QCOMPARE(md1.entryList().count(), 0);
+
+    job = mStore->fetchItems(collection1);
+
+    spy = new QSignalSpy(job, SIGNAL(itemsReceived(Akonadi::Item::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    items = job->items();
+    QCOMPARE((int)items.count(), 0);
+    QCOMPARE(spy->count(), 0);
+
+    var = job->property("remoteIdToTagList");
+    QVERIFY(!var.isValid());
+
+    // test listing maildir with index
+    Collection collection2;
+    collection2.setName(QLatin1String("collection2"));
+    collection2.setRemoteId(QLatin1String("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->fetchItems(collection2);
+
+    spy = new QSignalSpy(job, SIGNAL(itemsReceived(Akonadi::Item::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    items = job->items();
+    QCOMPARE((int)items.count(), 4);
+    QCOMPARE(itemsFromSpy(spy), items);
+
+    entrySet = entrySet2;
+    QVERIFY(entrySet.remove(items[ 0 ].remoteId()));
+    QVERIFY(entrySet.remove(items[ 1 ].remoteId()));
+    QVERIFY(entrySet.remove(items[ 2 ].remoteId()));
+    QVERIFY(entrySet.remove(items[ 3 ].remoteId()));
+
+    QCOMPARE(items[ 0 ].parentCollection(), collection2);
+    QCOMPARE(items[ 1 ].parentCollection(), collection2);
+    QCOMPARE(items[ 2 ].parentCollection(), collection2);
+    QCOMPARE(items[ 3 ].parentCollection(), collection2);
+
+    QVERIFY(!items[ 0 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 1 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 2 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 3 ].hasPayload<KMime::Message::Ptr>());
+
+    // see data/README
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    var = job->property("remoteIdToTagList");
+    QVERIFY(!var.isValid());
+
+    // test listing empty maildir with index
+    Q_FOREACH (const QString &entry, entrySet2) {
+        QVERIFY(md2.removeEntry(entry));
+    }
+    QCOMPARE(md2.entryList().count(), 0);
+
+    job = mStore->fetchItems(collection2);
+
+    spy = new QSignalSpy(job, SIGNAL(itemsReceived(Akonadi::Item::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    items = job->items();
+    QCOMPARE((int)items.count(), 0);
+    QCOMPARE(spy->count(), 0);
+
+    var = job->property("remoteIdToTagList");
+    QVERIFY(!var.isValid());
+
+    // test listing maildir with index which has tags
+    Collection collection3;
+    collection3.setName(QLatin1String("collection3"));
+    collection3.setRemoteId(QLatin1String("collection3"));
+    collection3.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->fetchItems(collection3);
+
+    spy = new QSignalSpy(job, SIGNAL(itemsReceived(Akonadi::Item::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    items = job->items();
+    QCOMPARE((int)items.count(), 4);
+    QCOMPARE(itemsFromSpy(spy), items);
+
+    entrySet = entrySet3;
+    QVERIFY(entrySet.remove(items[ 0 ].remoteId()));
+    QVERIFY(entrySet.remove(items[ 1 ].remoteId()));
+    QVERIFY(entrySet.remove(items[ 2 ].remoteId()));
+    QVERIFY(entrySet.remove(items[ 3 ].remoteId()));
+
+    QCOMPARE(items[ 0 ].parentCollection(), collection3);
+    QCOMPARE(items[ 1 ].parentCollection(), collection3);
+    QCOMPARE(items[ 2 ].parentCollection(), collection3);
+    QCOMPARE(items[ 3 ].parentCollection(), collection3);
+
+    QVERIFY(!items[ 0 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 1 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 2 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 3 ].hasPayload<KMime::Message::Ptr>());
+
+    // see data/README
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+
+    // 2x \SEEN flags: 2x from index, none from file name
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    var = job->property("remoteIdToTagList");
+    QVERIFY(var.isValid());
+
+    // tagListHash.contains tests below needs sorting of entries,
+    // but libmaildir does not sort for performance reasons.
+    // TODO: Check should not depend on any specific ordering.
+    qSort(items.begin(), items.end(), itemLessThanByRemoteId);
+
+    tagListHash = var.value< QHash<QString, QVariant> >();
+    QCOMPARE((int)tagListHash.count(), 3);
+    QVERIFY(!tagListHash.contains(items[ 0 ].remoteId()));
+    QVERIFY(!tagListHash.value(items[ 1 ].remoteId()).toString().isEmpty());
+    QVERIFY(!tagListHash.value(items[ 2 ].remoteId()).toString().isEmpty());
+    QVERIFY(!tagListHash.value(items[ 3 ].remoteId()).toString().isEmpty());
+
+    // test listing maildir with index which contains IMAP UIDs (dimap cache directory)
+    Collection collection4;
+    collection4.setName(QLatin1String("collection4"));
+    collection4.setRemoteId(QLatin1String("collection4"));
+    collection4.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->fetchItems(collection4);
+
+    spy = new QSignalSpy(job, SIGNAL(itemsReceived(Akonadi::Item::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    items = job->items();
+    QCOMPARE((int)items.count(), 4);
+    QCOMPARE(itemsFromSpy(spy), items);
+
+    entrySet = entrySet4;
+    QVERIFY(entrySet.remove(items[ 0 ].remoteId()));
+    QVERIFY(entrySet.remove(items[ 1 ].remoteId()));
+    QVERIFY(entrySet.remove(items[ 2 ].remoteId()));
+    QVERIFY(entrySet.remove(items[ 3 ].remoteId()));
+
+    QCOMPARE(items[ 0 ].parentCollection(), collection4);
+    QCOMPARE(items[ 1 ].parentCollection(), collection4);
+    QCOMPARE(items[ 2 ].parentCollection(), collection4);
+    QCOMPARE(items[ 3 ].parentCollection(), collection4);
+
+    QVERIFY(!items[ 0 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 1 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 2 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 3 ].hasPayload<KMime::Message::Ptr>());
+
+    // see data/README
+    Q_FOREACH (const Item &item, items) {
+        Q_FOREACH (const QByteArray &flag, item.flags()) {
+            ++flagCounts[ flag ];
+        }
+    }
+    QCOMPARE(flagCounts[ "\\SEEN" ], 2);
+    QCOMPARE(flagCounts[ "\\FLAGGED" ], 1);
+    QCOMPARE(flagCounts[ "$TODO" ], 1);
+    flagCounts.clear();
+
+    var = job->property("remoteIdToTagList");
+    QVERIFY(!var.isValid());
+
+    var = job->property("remoteIdToIndexUid");
+    QVERIFY(var.isValid());
+
+    uidHash = var.value< QHash<QString, QVariant> >();
+    QCOMPARE((int)uidHash.count(), 4);
+    bool ok = false;
+    QVERIFY(!uidHash.value(items[ 0 ].remoteId()).toString().toInt(&ok) >= 0 && ok);
+    QVERIFY(!uidHash.value(items[ 1 ].remoteId()).toString().toInt(&ok) >= 0 && ok);
+    QVERIFY(!uidHash.value(items[ 2 ].remoteId()).toString().toInt(&ok) >= 0 && ok);
+    QVERIFY(!uidHash.value(items[ 3 ].remoteId()).toString().toInt(&ok) >= 0 && ok);
+
+    // test listing maildir with index but newer modification date than index's one
+    const QByteArray data5 = md5.readEntry(*entrySet5.cbegin());
+    QVERIFY(!data5.isEmpty());
+
+    QTest::qSleep(1000);
+
+    QString newRemoteId = md5.addEntry(data5);
+    QVERIFY(!newRemoteId.isEmpty());
+
+    entrySet = QSet<QString>::fromList(md5.entryList());
+    entrySet.remove(newRemoteId);
+    QCOMPARE(entrySet, entrySet5);
+    QFileInfo fileInfo5(md5.path(), QLatin1String("new"));
+    QFileInfo indexFileInfo5 = indexFile(QFileInfo(md5.path()));
+    QVERIFY(fileInfo5.lastModified() > indexFileInfo5.lastModified());
+
+    Collection collection5;
+    collection5.setName(QLatin1String("collection5"));
+    collection5.setRemoteId(QLatin1String("collection5"));
+    collection5.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->fetchItems(collection5);
+
+    spy = new QSignalSpy(job, SIGNAL(itemsReceived(Akonadi::Item::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    items = job->items();
+    QCOMPARE((int)items.count(), 5);
+    QCOMPARE(itemsFromSpy(spy), items);
+
+    entrySet = entrySet5;
+    entrySet << newRemoteId;
+    QVERIFY(entrySet.remove(items[ 0 ].remoteId()));
+    QVERIFY(entrySet.remove(items[ 1 ].remoteId()));
+    QVERIFY(entrySet.remove(items[ 2 ].remoteId()));
+    QVERIFY(entrySet.remove(items[ 3 ].remoteId()));
+    QVERIFY(entrySet.remove(items[ 4 ].remoteId()));
+
+    QCOMPARE(items[ 0 ].parentCollection(), collection5);
+    QCOMPARE(items[ 1 ].parentCollection(), collection5);
+    QCOMPARE(items[ 2 ].parentCollection(), collection5);
+    QCOMPARE(items[ 3 ].parentCollection(), collection5);
+    QCOMPARE(items[ 4 ].parentCollection(), collection5);
+
+    QVERIFY(!items[ 0 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 1 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 2 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 3 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 4 ].hasPayload<KMime::Message::Ptr>());
+
+    // not flags from index, no flags from file names
+    QCOMPARE(items[ 0 ].flags(), QSet<QByteArray>());
+    QCOMPARE(items[ 1 ].flags(), QSet<QByteArray>());
+    QCOMPARE(items[ 2 ].flags(), QSet<QByteArray>());
+    QCOMPARE(items[ 3 ].flags(), QSet<QByteArray>());
+    QCOMPARE(items[ 4 ].flags(), QSet<QByteArray>());
+
+    var = job->property("remoteIdToTagList");
+    QVERIFY(!var.isValid());
+}
+
+void ItemFetchTest::testListingMBox()
+{
+    QDir topDir(mDir->path());
+
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox"), topDir.path(), QLatin1String("collection1")));
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox"), topDir.path(), QLatin1String("collection2")));
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox-tagged"), topDir.path(), QLatin1String("collection3")));
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox-unpurged"), topDir.path(), QLatin1String("collection4")));
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox-tagged"), topDir.path(), QLatin1String("collection5")));
+
+    QFileInfo fileInfo1(topDir.path(), QLatin1String("collection1"));
+    MBox mbox1;
+    QVERIFY(mbox1.load(fileInfo1.absoluteFilePath()));
+    MBoxEntry::List entryList1 = mbox1.entries();
+    QCOMPARE((int)entryList1.count(), 4);
+
+    QFileInfo indexFileInfo1 = indexFile(fileInfo1);
+    QVERIFY(QFile::remove(indexFileInfo1.absoluteFilePath()));
+
+    QFileInfo fileInfo2(topDir.path(), QLatin1String("collection2"));
+    MBox mbox2;
+    QVERIFY(mbox2.load(fileInfo2.absoluteFilePath()));
+    MBoxEntry::List entryList2 = mbox2.entries();
+    QCOMPARE((int)entryList2.count(), 4);
+
+    QFileInfo fileInfo3(topDir.path(), QLatin1String("collection3"));
+    MBox mbox3;
+    QVERIFY(mbox3.load(fileInfo3.absoluteFilePath()));
+    MBoxEntry::List entryList3 = mbox3.entries();
+    QCOMPARE((int)entryList3.count(), 4);
+
+    QFileInfo fileInfo4(topDir.path(), QLatin1String("collection4"));
+    MBox mbox4;
+    QVERIFY(mbox4.load(fileInfo4.absoluteFilePath()));
+    MBoxEntry::List entryList4 = mbox4.entries();
+    QCOMPARE((int)entryList4.count(), 4);
+
+    QFileInfo fileInfo5(topDir.path(), QLatin1String("collection5"));
+    MBox mbox5;
+    QVERIFY(mbox5.load(fileInfo5.absoluteFilePath()));
+    MBoxEntry::List entryList5 = mbox5.entries();
+    QCOMPARE((int)entryList5.count(), 4);
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    FileStore::ItemFetchJob *job = 0;
+
+    QSignalSpy *spy = 0;
+    Item::List items;
+
+    QHash<QString, QVariant> tagListHash;
+    const QVariant varTagListHash = QVariant::fromValue< QHash<QString, QVariant> >(tagListHash);
+    QVariant var;
+
+    // test listing mbox without index
+    Collection collection1;
+    collection1.setName(QLatin1String("collection1"));
+    collection1.setRemoteId(QLatin1String("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->fetchItems(collection1);
+
+    spy = new QSignalSpy(job, SIGNAL(itemsReceived(Akonadi::Item::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    items = job->items();
+    QCOMPARE((int)items.count(), 4);
+    QCOMPARE(itemsFromSpy(spy), items);
+
+    QCOMPARE(items[ 0 ].remoteId(), QString::number(entryList1[ 0 ].messageOffset()));
+    QCOMPARE(items[ 1 ].remoteId(), QString::number(entryList1[ 1 ].messageOffset()));
+    QCOMPARE(items[ 2 ].remoteId(), QString::number(entryList1[ 2 ].messageOffset()));
+    QCOMPARE(items[ 3 ].remoteId(), QString::number(entryList1[ 3 ].messageOffset()));
+
+    QCOMPARE(items[ 0 ].parentCollection(), collection1);
+    QCOMPARE(items[ 1 ].parentCollection(), collection1);
+    QCOMPARE(items[ 2 ].parentCollection(), collection1);
+    QCOMPARE(items[ 3 ].parentCollection(), collection1);
+
+    QVERIFY(!items[ 0 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 1 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 2 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 3 ].hasPayload<KMime::Message::Ptr>());
+
+    QCOMPARE(items[ 0 ].flags(), QSet<QByteArray>());
+    QCOMPARE(items[ 1 ].flags(), QSet<QByteArray>());
+    QCOMPARE(items[ 2 ].flags(), QSet<QByteArray>());
+    QCOMPARE(items[ 3 ].flags(), QSet<QByteArray>());
+
+    var = job->property("remoteIdToTagList");
+    QVERIFY(!var.isValid());
+
+    // test listing empty mbox without index
+    QFile file1(fileInfo1.absoluteFilePath());
+    QVERIFY(file1.open(QIODevice::WriteOnly | QIODevice::Truncate));
+    file1.close();
+    QCOMPARE((int)file1.size(), 0);
+
+    job = mStore->fetchItems(collection1);
+
+    spy = new QSignalSpy(job, SIGNAL(itemsReceived(Akonadi::Item::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    items = job->items();
+    QCOMPARE((int)items.count(), 0);
+    QCOMPARE(spy->count(), 0);
+
+    var = job->property("remoteIdToTagList");
+    QVERIFY(!var.isValid());
+
+    // test listing mbox with index
+    Collection collection2;
+    collection2.setName(QLatin1String("collection2"));
+    collection2.setRemoteId(QLatin1String("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->fetchItems(collection2);
+
+    spy = new QSignalSpy(job, SIGNAL(itemsReceived(Akonadi::Item::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    items = job->items();
+    QCOMPARE((int)items.count(), 4);
+    QCOMPARE(itemsFromSpy(spy), items);
+
+    QCOMPARE(items[ 0 ].remoteId(), QString::number(entryList2[ 0 ].messageOffset()));
+    QCOMPARE(items[ 1 ].remoteId(), QString::number(entryList2[ 1 ].messageOffset()));
+    QCOMPARE(items[ 2 ].remoteId(), QString::number(entryList2[ 2 ].messageOffset()));
+    QCOMPARE(items[ 3 ].remoteId(), QString::number(entryList2[ 3 ].messageOffset()));
+
+    QCOMPARE(items[ 0 ].parentCollection(), collection2);
+    QCOMPARE(items[ 1 ].parentCollection(), collection2);
+    QCOMPARE(items[ 2 ].parentCollection(), collection2);
+    QCOMPARE(items[ 3 ].parentCollection(), collection2);
+
+    QVERIFY(!items[ 0 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 1 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 2 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 3 ].hasPayload<KMime::Message::Ptr>());
+
+    // see data/README
+    QCOMPARE(items[ 0 ].flags(), QSet<QByteArray>());
+    QCOMPARE(items[ 1 ].flags(), QSet<QByteArray>() << "\\SEEN" << "$TODO");
+    QCOMPARE(items[ 2 ].flags(), QSet<QByteArray>());
+    QCOMPARE(items[ 3 ].flags(), QSet<QByteArray>() << "\\SEEN" << "\\FLAGGED");
+
+    var = job->property("remoteIdToTagList");
+    QVERIFY(!var.isValid());
+
+    // test listing empty mbox with index
+    QFile file2(fileInfo2.absoluteFilePath());
+    QVERIFY(file2.open(QIODevice::WriteOnly | QIODevice::Truncate));
+    file2.close();
+    QCOMPARE((int)file2.size(), 0);
+
+    job = mStore->fetchItems(collection2);
+
+    spy = new QSignalSpy(job, SIGNAL(itemsReceived(Akonadi::Item::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    items = job->items();
+    QCOMPARE((int)items.count(), 0);
+    QCOMPARE(spy->count(), 0);
+
+    var = job->property("remoteIdToTagList");
+    QVERIFY(!var.isValid());
+
+    // test listing mbox with index which has tags
+    Collection collection3;
+    collection3.setName(QLatin1String("collection3"));
+    collection3.setRemoteId(QLatin1String("collection3"));
+    collection3.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->fetchItems(collection3);
+
+    spy = new QSignalSpy(job, SIGNAL(itemsReceived(Akonadi::Item::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    items = job->items();
+    QCOMPARE((int)items.count(), 4);
+    QCOMPARE(itemsFromSpy(spy), items);
+
+    QCOMPARE(items[ 0 ].remoteId(), QString::number(entryList3[ 0 ].messageOffset()));
+    QCOMPARE(items[ 1 ].remoteId(), QString::number(entryList3[ 1 ].messageOffset()));
+    QCOMPARE(items[ 2 ].remoteId(), QString::number(entryList3[ 2 ].messageOffset()));
+    QCOMPARE(items[ 3 ].remoteId(), QString::number(entryList3[ 3 ].messageOffset()));
+
+    QCOMPARE(items[ 0 ].parentCollection(), collection3);
+    QCOMPARE(items[ 1 ].parentCollection(), collection3);
+    QCOMPARE(items[ 2 ].parentCollection(), collection3);
+    QCOMPARE(items[ 3 ].parentCollection(), collection3);
+
+    QVERIFY(!items[ 0 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 1 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 2 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 3 ].hasPayload<KMime::Message::Ptr>());
+
+    // see data/README
+    QCOMPARE(items[ 0 ].flags(), QSet<QByteArray>());
+    QCOMPARE(items[ 1 ].flags(), QSet<QByteArray>() << "\\SEEN" << "$TODO");
+    QCOMPARE(items[ 2 ].flags(), QSet<QByteArray>());
+    QCOMPARE(items[ 3 ].flags(), QSet<QByteArray>() << "\\SEEN" << "\\FLAGGED");
+
+    var = job->property("remoteIdToTagList");
+    QVERIFY(var.isValid());
+
+    tagListHash = var.value< QHash<QString, QVariant> >();
+    QCOMPARE((int)tagListHash.count(), 3);
+    QVERIFY(!tagListHash.value(items[ 0 ].remoteId()).toString().isEmpty());
+    QVERIFY(!tagListHash.contains(items[ 1 ].remoteId()));
+    QVERIFY(!tagListHash.value(items[ 2 ].remoteId()).toString().isEmpty());
+    QVERIFY(!tagListHash.value(items[ 3 ].remoteId()).toString().isEmpty());
+
+    // test listing mbox with index and unpurged messages (in mbox but not in index)
+    Collection collection4;
+    collection4.setName(QLatin1String("collection4"));
+    collection4.setRemoteId(QLatin1String("collection4"));
+    collection4.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->fetchItems(collection4);
+
+    spy = new QSignalSpy(job, SIGNAL(itemsReceived(Akonadi::Item::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    items = job->items();
+    QCOMPARE((int)items.count(), 4);
+    QCOMPARE(itemsFromSpy(spy), items);
+
+    QCOMPARE(items[ 0 ].remoteId(), QString::number(entryList4[ 0 ].messageOffset()));
+    QCOMPARE(items[ 1 ].remoteId(), QString::number(entryList4[ 1 ].messageOffset()));
+    QCOMPARE(items[ 2 ].remoteId(), QString::number(entryList4[ 2 ].messageOffset()));
+    QCOMPARE(items[ 3 ].remoteId(), QString::number(entryList4[ 3 ].messageOffset()));
+
+    QCOMPARE(items[ 0 ].parentCollection(), collection4);
+    QCOMPARE(items[ 1 ].parentCollection(), collection4);
+    QCOMPARE(items[ 2 ].parentCollection(), collection4);
+    QCOMPARE(items[ 3 ].parentCollection(), collection4);
+
+    QVERIFY(!items[ 0 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 1 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 2 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 3 ].hasPayload<KMime::Message::Ptr>());
+
+    // see data/README
+    QCOMPARE(items[ 0 ].flags(), QSet<QByteArray>() << "\\SEEN");
+    QCOMPARE(items[ 1 ].flags(), QSet<QByteArray>() << "\\DELETED");
+    QCOMPARE(items[ 2 ].flags(), QSet<QByteArray>() << "\\DELETED");
+    QCOMPARE(items[ 3 ].flags(), QSet<QByteArray>() << "\\SEEN");
+
+    var = job->property("remoteIdToTagList");
+    QVERIFY(!var.isValid());
+
+    // test listing mbox with index but newer modification date than index's one
+    QFile file5(fileInfo5.absoluteFilePath());
+    QVERIFY(file5.open(QIODevice::ReadOnly));
+    const QByteArray data5 = file5.readAll();
+    file5.close();
+
+    QTest::qSleep(1000);
+
+    QVERIFY(file5.open(QIODevice::WriteOnly));
+    file5.write(data5);
+    file5.close();
+
+    QCOMPARE(file5.size(), fileInfo5.size());
+    fileInfo5.refresh();
+    QFileInfo indexFileInfo5 = indexFile(fileInfo5);
+    QVERIFY(fileInfo5.lastModified() > indexFileInfo5.lastModified());
+
+    Collection collection5;
+    collection5.setName(QLatin1String("collection5"));
+    collection5.setRemoteId(QLatin1String("collection5"));
+    collection5.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->fetchItems(collection5);
+
+    spy = new QSignalSpy(job, SIGNAL(itemsReceived(Akonadi::Item::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    items = job->items();
+    QCOMPARE((int)items.count(), 4);
+    QCOMPARE(itemsFromSpy(spy), items);
+
+    QCOMPARE(items[ 0 ].remoteId(), QString::number(entryList5[ 0 ].messageOffset()));
+    QCOMPARE(items[ 1 ].remoteId(), QString::number(entryList5[ 1 ].messageOffset()));
+    QCOMPARE(items[ 2 ].remoteId(), QString::number(entryList5[ 2 ].messageOffset()));
+    QCOMPARE(items[ 3 ].remoteId(), QString::number(entryList5[ 3 ].messageOffset()));
+
+    QCOMPARE(items[ 0 ].parentCollection(), collection5);
+    QCOMPARE(items[ 1 ].parentCollection(), collection5);
+    QCOMPARE(items[ 2 ].parentCollection(), collection5);
+    QCOMPARE(items[ 3 ].parentCollection(), collection5);
+
+    QVERIFY(!items[ 0 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 1 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 2 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 3 ].hasPayload<KMime::Message::Ptr>());
+
+    QCOMPARE(items[ 0 ].flags(), QSet<QByteArray>());
+    QCOMPARE(items[ 1 ].flags(), QSet<QByteArray>());
+    QCOMPARE(items[ 2 ].flags(), QSet<QByteArray>());
+    QCOMPARE(items[ 3 ].flags(), QSet<QByteArray>());
+
+    var = job->property("remoteIdToTagList");
+    QVERIFY(!var.isValid());
+
+    // test that a new message in an mbox with index it not marked as deleted
+    KMime::Message::Ptr msgPtr(new KMime::Message);
+    msgPtr->subject()->from7BitString("Test 5");
+    msgPtr->to()->from7BitString("kevin.krammer@gmx.at");
+    msgPtr->assemble();
+
+    Item item3_5;
+    item3_5.setMimeType(KMime::Message::mimeType());
+    item3_5.setPayload<KMime::Message::Ptr>(msgPtr);
+
+    FileStore::ItemCreateJob *itemCreate = mStore->createItem(item3_5, collection3);
+    QVERIFY(itemCreate->exec());
+
+    item3_5 = itemCreate->item();
+    QVERIFY(!item3_5.remoteId().isEmpty());
+
+    job = mStore->fetchItems(collection3);
+
+    spy = new QSignalSpy(job, SIGNAL(itemsReceived(Akonadi::Item::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    items = job->items();
+    QCOMPARE((int)items.count(), 5);
+    QCOMPARE(itemsFromSpy(spy), items);
+
+    QCOMPARE(items[ 0 ].remoteId(), QString::number(entryList3[ 0 ].messageOffset()));
+    QCOMPARE(items[ 1 ].remoteId(), QString::number(entryList3[ 1 ].messageOffset()));
+    QCOMPARE(items[ 2 ].remoteId(), QString::number(entryList3[ 2 ].messageOffset()));
+    QCOMPARE(items[ 3 ].remoteId(), QString::number(entryList3[ 3 ].messageOffset()));
+    QCOMPARE(items[ 4 ].remoteId(), item3_5.remoteId());
+
+    QCOMPARE(items[ 0 ].parentCollection(), collection3);
+    QCOMPARE(items[ 1 ].parentCollection(), collection3);
+    QCOMPARE(items[ 2 ].parentCollection(), collection3);
+    QCOMPARE(items[ 3 ].parentCollection(), collection3);
+    QCOMPARE(items[ 4 ].parentCollection(), collection3);
+
+    QVERIFY(!items[ 0 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 1 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 2 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 3 ].hasPayload<KMime::Message::Ptr>());
+    QVERIFY(!items[ 4 ].hasPayload<KMime::Message::Ptr>());
+
+    // see data/README
+    QCOMPARE(items[ 0 ].flags(), QSet<QByteArray>());
+    QCOMPARE(items[ 1 ].flags(), QSet<QByteArray>() << "\\SEEN" << "$TODO");
+    QCOMPARE(items[ 2 ].flags(), QSet<QByteArray>());
+    QCOMPARE(items[ 3 ].flags(), QSet<QByteArray>() << "\\SEEN" << "\\FLAGGED");
+    QCOMPARE(items[ 4 ].flags(), QSet<QByteArray>());
+
+    var = job->property("remoteIdToTagList");
+    QVERIFY(var.isValid());
+}
+
+void ItemFetchTest::testSingleItemFetchMaildir()
+{
+    QDir topDir(mDir->path());
+
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("maildir"), topDir.path(), QLatin1String("collection1")));
+
+    KPIM::Maildir topLevelMd(topDir.path(), true);
+
+    KPIM::Maildir md1 = topLevelMd.subFolder(QLatin1String("collection1"));
+    QStringList entryList1 = md1.entryList();
+    QCOMPARE((int)entryList1.count(), 4);
+
+    KRandomSequence randomSequence;
+    QStringList randomList1 = entryList1;
+    randomSequence.randomize(randomList1);
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    FileStore::ItemFetchJob *job = 0;
+
+    QSignalSpy *spy = 0;
+    Item::List items;
+
+    // test fetching from maildir, headers only
+    Collection collection1;
+    collection1.setName(QLatin1String("collection1"));
+    collection1.setRemoteId(QLatin1String("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    Q_FOREACH (const QString &entry, randomList1) {
+        Item item1;
+        item1.setId(KRandom::random());
+        item1.setRemoteId(entry);
+        item1.setParentCollection(collection1);
+
+        job = mStore->fetchItem(item1);
+        job->fetchScope().fetchPayloadPart(MessagePart::Header);
+
+        spy = new QSignalSpy(job, SIGNAL(itemsReceived(Akonadi::Item::List)));
+
+        QVERIFY(job->exec());
+        QCOMPARE(job->error(), 0);
+
+        items = job->items();
+        QCOMPARE((int)items.count(), 1);
+        QCOMPARE(itemsFromSpy(spy), items);
+
+        Item item = items.first();
+        QCOMPARE(item, item1);
+        QVERIFY(item.hasPayload<KMime::Message::Ptr>());
+
+        KMime::Message::Ptr msgPtr = item.payload<KMime::Message::Ptr>();
+        QVERIFY(msgPtr != 0);
+
+        const QSet<QByteArray> parts = messageParts(msgPtr);
+        QVERIFY(!parts.isEmpty());
+        QVERIFY(parts.contains(MessagePart::Header));
+        QVERIFY(!parts.contains(MessagePart::Body));
+    }
+
+    // test fetching from maildir, including body
+    randomSequence.randomize(randomList1);
+    Q_FOREACH (const QString &entry, randomList1) {
+        Item item1;
+        item1.setId(KRandom::random());
+        item1.setRemoteId(entry);
+        item1.setParentCollection(collection1);
+
+        job = mStore->fetchItem(item1);
+        job->fetchScope().fetchPayloadPart(MessagePart::Header);
+        job->fetchScope().fetchPayloadPart(MessagePart::Body);
+
+        spy = new QSignalSpy(job, SIGNAL(itemsReceived(Akonadi::Item::List)));
+
+        QVERIFY(job->exec());
+        QCOMPARE(job->error(), 0);
+
+        items = job->items();
+        QCOMPARE((int)items.count(), 1);
+        QCOMPARE(itemsFromSpy(spy), items);
+
+        Item item = items.first();
+        QCOMPARE(item, item1);
+        QVERIFY(item.hasPayload<KMime::Message::Ptr>());
+
+        KMime::Message::Ptr msgPtr = item.payload<KMime::Message::Ptr>();
+        QVERIFY(msgPtr != 0);
+
+        const QSet<QByteArray> parts = messageParts(msgPtr);
+        QVERIFY(!parts.isEmpty());
+        QVERIFY(parts.contains(MessagePart::Header));
+        QVERIFY(parts.contains(MessagePart::Body));
+    }
+
+    // test fetching from maildir, just specifying full payload
+    randomSequence.randomize(randomList1);
+    Q_FOREACH (const QString &entry, randomList1) {
+        Item item1;
+        item1.setId(KRandom::random());
+        item1.setRemoteId(entry);
+        item1.setParentCollection(collection1);
+
+        job = mStore->fetchItem(item1);
+        job->fetchScope().fetchFullPayload(true);
+
+        spy = new QSignalSpy(job, SIGNAL(itemsReceived(Akonadi::Item::List)));
+
+        QVERIFY(job->exec());
+        QCOMPARE(job->error(), 0);
+
+        items = job->items();
+        QCOMPARE((int)items.count(), 1);
+        QCOMPARE(itemsFromSpy(spy), items);
+
+        Item item = items.first();
+        QCOMPARE(item, item1);
+        QVERIFY(item.hasPayload<KMime::Message::Ptr>());
+
+        KMime::Message::Ptr msgPtr = item.payload<KMime::Message::Ptr>();
+        QVERIFY(msgPtr != 0);
+
+        const QSet<QByteArray> parts = messageParts(msgPtr);
+        QVERIFY(!parts.isEmpty());
+        QVERIFY(parts.contains(MessagePart::Header));
+        QVERIFY(parts.contains(MessagePart::Body));
+    }
+}
+
+void ItemFetchTest::testSingleItemFetchMBox()
+{
+    QDir topDir(mDir->path());
+
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox"), topDir.path(), QLatin1String("collection1")));
+    // one message has no body
+    const QByteArray messageIdOfEmptyBodyMsg = "201007241551.37547.kevin.krammer@demo.kolab.org";
+
+    QFileInfo fileInfo1(topDir.path(), QLatin1String("collection1"));
+    MBox mbox1;
+    QVERIFY(mbox1.load(fileInfo1.absoluteFilePath()));
+    MBoxEntry::List entryList1 = mbox1.entries();
+    QCOMPARE((int)entryList1.count(), 4);
+
+    KRandomSequence randomSequence;
+    MBoxEntry::List randomList1 = entryList1;
+    randomSequence.randomize(randomList1);
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    FileStore::ItemFetchJob *job = 0;
+
+    QSignalSpy *spy = 0;
+    Item::List items;
+
+    // test fetching from mbox, headers only
+    Collection collection1;
+    collection1.setName(QLatin1String("collection1"));
+    collection1.setRemoteId(QLatin1String("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    Q_FOREACH (const MBoxEntry &entry, randomList1) {
+        Item item1;
+        item1.setId(KRandom::random());
+        item1.setRemoteId(QString::number(entry.messageOffset()));
+        item1.setParentCollection(collection1);
+
+        job = mStore->fetchItem(item1);
+        job->fetchScope().fetchPayloadPart(MessagePart::Header);
+
+        spy = new QSignalSpy(job, SIGNAL(itemsReceived(Akonadi::Item::List)));
+
+        QVERIFY(job->exec());
+        QCOMPARE(job->error(), 0);
+
+        items = job->items();
+        QCOMPARE((int)items.count(), 1);
+        QCOMPARE(itemsFromSpy(spy), items);
+
+        Item item = items.first();
+        QCOMPARE(item, item1);
+        QVERIFY(item.hasPayload<KMime::Message::Ptr>());
+
+        KMime::Message::Ptr msgPtr = item.payload<KMime::Message::Ptr>();
+        QVERIFY(msgPtr != 0);
+
+        const QSet<QByteArray> parts = messageParts(msgPtr);
+        QVERIFY(!parts.isEmpty());
+        QVERIFY(parts.contains(MessagePart::Header));
+        QVERIFY(!parts.contains(MessagePart::Body));
+    }
+
+    // test fetching from mbox, including body
+    randomSequence.randomize(randomList1);
+    Q_FOREACH (const MBoxEntry &entry, randomList1) {
+        Item item1;
+        item1.setId(KRandom::random());
+        item1.setRemoteId(QString::number(entry.messageOffset()));
+        item1.setParentCollection(collection1);
+
+        job = mStore->fetchItem(item1);
+        job->fetchScope().fetchPayloadPart(MessagePart::Header);
+        job->fetchScope().fetchPayloadPart(MessagePart::Body);
+
+        spy = new QSignalSpy(job, SIGNAL(itemsReceived(Akonadi::Item::List)));
+
+        QVERIFY(job->exec());
+        QCOMPARE(job->error(), 0);
+
+        items = job->items();
+        QCOMPARE((int)items.count(), 1);
+        QCOMPARE(itemsFromSpy(spy), items);
+
+        Item item = items.first();
+        QCOMPARE(item, item1);
+        QVERIFY(item.hasPayload<KMime::Message::Ptr>());
+
+        KMime::Message::Ptr msgPtr = item.payload<KMime::Message::Ptr>();
+        QVERIFY(msgPtr != 0);
+
+        const QSet<QByteArray> parts = messageParts(msgPtr);
+        qCDebug(MIXEDMAILDIRRESOURCE_LOG) << msgPtr->messageID()->identifier();
+        QVERIFY(!parts.isEmpty());
+        QVERIFY(parts.contains(MessagePart::Header));
+        if (msgPtr->messageID()->identifier() == messageIdOfEmptyBodyMsg) {
+            QVERIFY(!parts.contains(MessagePart::Body));
+        } else {
+            QVERIFY(parts.contains(MessagePart::Body));
+        }
+    }
+
+    // test fetching from mbox, just specifying full payload
+    randomSequence.randomize(randomList1);
+    Q_FOREACH (const MBoxEntry &entry, randomList1) {
+        Item item1;
+        item1.setId(KRandom::random());
+        item1.setRemoteId(QString::number(entry.messageOffset()));
+        item1.setParentCollection(collection1);
+
+        job = mStore->fetchItem(item1);
+        job->fetchScope().fetchFullPayload(true);
+
+        spy = new QSignalSpy(job, SIGNAL(itemsReceived(Akonadi::Item::List)));
+
+        QVERIFY(job->exec());
+        QCOMPARE(job->error(), 0);
+
+        items = job->items();
+        QCOMPARE((int)items.count(), 1);
+        QCOMPARE(itemsFromSpy(spy), items);
+
+        Item item = items.first();
+        QCOMPARE(item, item1);
+        QVERIFY(item.hasPayload<KMime::Message::Ptr>());
+
+        KMime::Message::Ptr msgPtr = item.payload<KMime::Message::Ptr>();
+        QVERIFY(msgPtr != 0);
+
+        const QSet<QByteArray> parts = messageParts(msgPtr);
+        QVERIFY(!parts.isEmpty());
+        QVERIFY(parts.contains(MessagePart::Header));
+        if (msgPtr->messageID()->identifier() == messageIdOfEmptyBodyMsg) {
+            QVERIFY(!parts.contains(MessagePart::Body));
+        } else {
+            QVERIFY(parts.contains(MessagePart::Body));
+        }
+    }
+}
+
+QTEST_MAIN(ItemFetchTest)
+
+#include "itemfetchtest.moc"
+
diff --git a/resources/mixedmaildir/autotests/itemmodifytest.cpp b/resources/mixedmaildir/autotests/itemmodifytest.cpp
new file mode 100644 (file)
index 0000000..0c2e33c
--- /dev/null
@@ -0,0 +1,671 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+    Copyright (C) 2011 Kevin Krammer, kevin.krammer@gmx.at
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "mixedmaildirstore.h"
+
+#include "testdatautil.h"
+
+#include "filestore/itemfetchjob.h"
+#include "filestore/itemmodifyjob.h"
+#include "filestore/storecompactjob.h"
+
+#include "libmaildir/maildir.h"
+
+#include <akonadi/kmime/messageparts.h>
+#include <itemfetchscope.h>
+
+#include <kmbox/mbox.h>
+#include <kmime/kmime_message.h>
+
+#include <KRandom>
+#include <QTemporaryDir>
+
+#include <qtest.h>
+#include <QFileInfo>
+#include <QDir>
+#include <QCryptographicHash>
+
+using namespace Akonadi;
+using namespace KMBox;
+
+static bool fullEntryCompare(const MBoxEntry &a, const MBoxEntry &b)
+{
+    return a.messageOffset() == b.messageOffset() &&
+           a.separatorSize() == b.separatorSize() &&
+           a.messageSize() == b.messageSize();
+}
+
+class ItemModifyTest : public QObject
+{
+    Q_OBJECT
+
+public:
+    ItemModifyTest()
+        : QObject(), mStore(0), mDir(0) {}
+
+    ~ItemModifyTest()
+    {
+        delete mStore;
+        delete mDir;
+    }
+
+private:
+    MixedMaildirStore *mStore;
+    QTemporaryDir *mDir;
+
+private Q_SLOTS:
+    void init();
+    void cleanup();
+    void testExpectedFail();
+    void testIgnorePayload();
+    void testModifyPayload();
+    void testModifyFlags();
+    void testModifyFlagsAndPayload();
+};
+
+void ItemModifyTest::init()
+{
+    mStore = new MixedMaildirStore;
+
+    mDir = new QTemporaryDir;
+    QVERIFY(mDir->isValid());
+    QVERIFY(QDir(mDir->path()).exists());
+
+}
+
+void ItemModifyTest::cleanup()
+{
+    delete mStore;
+    mStore = 0;
+    delete mDir;
+    mDir = 0;
+}
+
+void ItemModifyTest::testExpectedFail()
+{
+    QDir topDir(mDir->path());
+
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("maildir"), topDir.path(), QStringLiteral("collection1")));
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox"), topDir.path(), QStringLiteral("collection2")));
+
+    KPIM::Maildir topLevelMd(topDir.path(), true);
+
+    KPIM::Maildir md1 = topLevelMd.subFolder(QStringLiteral("collection1"));
+    QSet<QString> entrySet1 = QSet<QString>::fromList(md1.entryList());
+    QCOMPARE((int)entrySet1.count(), 4);
+
+    QFileInfo fileInfo2(topDir.path(), QStringLiteral("collection2"));
+    MBox mbox2;
+    QVERIFY(mbox2.load(fileInfo2.absoluteFilePath()));
+    MBoxEntry::List entryList2 = mbox2.entries();
+    QCOMPARE((int)entryList2.count(), 4);
+
+    QSet<qint64> entrySet2;
+    Q_FOREACH (const MBoxEntry &entry, entryList2) {
+        entrySet2 << entry.messageOffset();
+    }
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    FileStore::ItemModifyJob *job = 0;
+
+    // test failure of modifying a non-existent maildir entry
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    QString remoteId1;
+    do {
+        remoteId1 = KRandom::randomString(20);
+    } while (entrySet1.contains(remoteId1));
+
+    KMime::Message::Ptr msgPtr(new KMime::Message);
+
+    Item item1;
+    item1.setMimeType(KMime::Message::mimeType());
+    item1.setRemoteId(remoteId1);
+    item1.setParentCollection(collection1);
+    item1.setPayload<KMime::Message::Ptr>(msgPtr);
+
+    job = mStore->modifyItem(item1);
+
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+
+    QSet<QString> entrySet = QSet<QString>::fromList(md1.entryList());
+    QCOMPARE(entrySet, entrySet1);
+
+    // test failure of modifying a non-existent mbox entry
+    Collection collection2;
+    collection2.setName(QStringLiteral("collection2"));
+    collection2.setRemoteId(QStringLiteral("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+
+    qint64 remoteId2;
+    do {
+        remoteId2 = KRandom::random();
+    } while (entrySet2.contains(remoteId2));
+
+    Item item2;
+    item2.setMimeType(KMime::Message::mimeType());
+    item2.setRemoteId(QString::number(remoteId2));
+    item2.setParentCollection(collection2);
+    item2.setPayload<KMime::Message::Ptr>(msgPtr);
+
+    job = mStore->modifyItem(item2);
+
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+
+    QVERIFY(mbox2.load(mbox2.fileName()));
+    MBoxEntry::List entryList = mbox2.entries();
+    QVERIFY(std::equal(entryList.begin(), entryList.end(), entryList2.begin(), fullEntryCompare));
+}
+
+void ItemModifyTest::testIgnorePayload()
+{
+    QDir topDir(mDir->path());
+
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("maildir"), topDir.path(), QStringLiteral("collection1")));
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox"), topDir.path(), QStringLiteral("collection2")));
+
+    KPIM::Maildir topLevelMd(topDir.path(), true);
+
+    KPIM::Maildir md1 = topLevelMd.subFolder(QStringLiteral("collection1"));
+    QStringList entryList1 = md1.entryList();
+    QCOMPARE((int)entryList1.count(), 4);
+
+    QFileInfo fileInfo2(topDir.path(), QStringLiteral("collection2"));
+    MBox mbox2;
+    QVERIFY(mbox2.load(fileInfo2.absoluteFilePath()));
+    MBoxEntry::List entryList2 = mbox2.entries();
+    QCOMPARE((int)entryList2.count(), 4);
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    FileStore::ItemModifyJob *job = 0;
+
+    // test failure of modifying a non-existent maildir entry
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    const QByteArray data1 = md1.readEntry(entryList1.first());
+
+    KMime::Message::Ptr msgPtr(new KMime::Message);
+    msgPtr->subject()->from7BitString("Modify Test");
+    msgPtr->assemble();
+
+    Item item1;
+    item1.setMimeType(KMime::Message::mimeType());
+    item1.setRemoteId(entryList1.first());
+    item1.setParentCollection(collection1);
+    item1.setPayload<KMime::Message::Ptr>(msgPtr);
+
+    job = mStore->modifyItem(item1);
+    job->setIgnorePayload(true);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    QCOMPARE(md1.entryList(), entryList1);
+    QCOMPARE(md1.readEntry(entryList1.first()), data1);
+
+    // test failure of modifying a non-existent mbox entry
+    Collection collection2;
+    collection2.setName(QStringLiteral("collection2"));
+    collection2.setRemoteId(QStringLiteral("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+
+    const QByteArray data2 = mbox2.readRawMessage(MBoxEntry(0));
+
+    Item item2;
+    item2.setMimeType(KMime::Message::mimeType());
+    item2.setRemoteId(QStringLiteral("0"));
+    item2.setParentCollection(collection2);
+    item2.setPayload<KMime::Message::Ptr>(msgPtr);
+
+    job = mStore->modifyItem(item2);
+    job->setIgnorePayload(true);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    QVERIFY(mbox2.load(mbox2.fileName()));
+    QCOMPARE(mbox2.entries(), entryList2);
+    QCOMPARE(mbox2.readRawMessage(MBoxEntry(0)), data2);
+}
+
+void ItemModifyTest::testModifyPayload()
+{
+    QDir topDir(mDir->path());
+
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("maildir"), topDir.path(), QStringLiteral("collection1")));
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox"), topDir.path(), QStringLiteral("collection2")));
+
+    KPIM::Maildir topLevelMd(topDir.path(), true);
+
+    KPIM::Maildir md1 = topLevelMd.subFolder(QStringLiteral("collection1"));
+    QStringList entryList1 = md1.entryList();
+    QCOMPARE((int)entryList1.count(), 4);
+
+    QFileInfo fileInfo2(topDir.path(), QStringLiteral("collection2"));
+    MBox mbox2;
+    QVERIFY(mbox2.load(fileInfo2.absoluteFilePath()));
+    MBoxEntry::List entryList2 = mbox2.entries();
+    QCOMPARE((int)entryList2.count(), 4);
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    FileStore::ItemModifyJob *job = 0;
+
+    const QVariant colListVar = QVariant::fromValue<Collection::List>(Collection::List());
+    QVariant var;
+    Collection::List collections;
+
+    // test modifying a maildir entry's header
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    const QByteArray data1 = md1.readEntry(entryList1.first());
+
+    KMime::Message::Ptr msgPtr(new KMime::Message);
+    msgPtr->setContent(KMime::CRLFtoLF(data1));
+    msgPtr->subject()->from7BitString("Modify Test");
+    msgPtr->assemble();
+
+    Item item1;
+    item1.setMimeType(KMime::Message::mimeType());
+    item1.setRemoteId(entryList1.first());
+    item1.setParentCollection(collection1);
+    item1.setPayload<KMime::Message::Ptr>(msgPtr);
+
+    job = mStore->modifyItem(item1);
+    // changing subject, so indicate a header change
+    job->setParts(QSet<QByteArray>() << QByteArray("PLD:") + QByteArray(MessagePart::Header));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    QCOMPARE(md1.entryList(), entryList1);
+
+    QCOMPARE(md1.readEntry(entryList1.first()).size(),
+             msgPtr->encodedContent().size());
+    QCOMPARE(md1.readEntry(entryList1.first()), msgPtr->encodedContent());
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection1);
+
+    // test modifying an mbox entry's header
+    Collection collection2;
+    collection2.setName(QStringLiteral("collection2"));
+    collection2.setRemoteId(QStringLiteral("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+
+    const QByteArray data2 = mbox2.readRawMessage(MBoxEntry(0));
+
+    msgPtr->setContent(KMime::CRLFtoLF(data2));
+    msgPtr->subject()->from7BitString("Modify Test");
+    msgPtr->assemble();
+
+    Item item2;
+    item2.setMimeType(KMime::Message::mimeType());
+    item2.setRemoteId(QStringLiteral("0"));
+    item2.setParentCollection(collection2);
+    item2.setPayload<KMime::Message::Ptr>(msgPtr);
+
+    job = mStore->modifyItem(item2);
+    // changing subject, so indicate a header change
+    job->setParts(QSet<QByteArray>() << QByteArray("PLD:") + QByteArray(MessagePart::Header));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    Item item = job->item();
+
+    QVERIFY(mbox2.load(mbox2.fileName()));
+    MBoxEntry::List entryList = mbox2.entries();
+    QCOMPARE((int)entryList.count(), 5);   // mbox file not purged yet
+    QCOMPARE(entryList.last().messageOffset(), item.remoteId().toULongLong());
+
+    var = job->property("compactStore");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.type(), QVariant::Bool);
+    QCOMPARE(var.toBool(), true);
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection2);
+
+    FileStore::ItemFetchJob *itemFetch = mStore->fetchItem(item2);
+    QVERIFY(!itemFetch->exec());   // item at old offset gone
+
+    FileStore::StoreCompactJob *storeCompact = mStore->compactStore();
+    QVERIFY(storeCompact->exec());
+
+    QVERIFY(mbox2.load(mbox2.fileName()));
+    entryList = mbox2.entries();
+    QCOMPARE((int)entryList.count(), 4);
+
+    QCOMPARE(mbox2.readRawMessage(entryList.last()), msgPtr->encodedContent());
+}
+
+void ItemModifyTest::testModifyFlags()
+{
+    QDir topDir(mDir->path());
+
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("maildir"), topDir.path(), QStringLiteral("collection1")));
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox"), topDir.path(), QStringLiteral("collection2")));
+
+    KPIM::Maildir topLevelMd(topDir.path(), true);
+
+    KPIM::Maildir md1 = topLevelMd.subFolder(QStringLiteral("collection1"));
+    QStringList entryList1 = md1.entryList();
+    QCOMPARE((int)entryList1.count(), 4);
+
+    QFileInfo fileInfo2(topDir.path(), QStringLiteral("collection2"));
+    MBox mbox2;
+    QVERIFY(mbox2.load(fileInfo2.absoluteFilePath()));
+    MBoxEntry::List entryList2 = mbox2.entries();
+    QCOMPARE((int)entryList2.count(), 4);
+
+    QCryptographicHash cryptoHash(QCryptographicHash::Sha1);
+
+    QFile file2(fileInfo2.absoluteFilePath());
+    QVERIFY(file2.open(QIODevice::ReadOnly));
+    cryptoHash.addData(file2.readAll());
+    const QByteArray mbox2Sha1 = cryptoHash.result();
+
+    file2.close();
+    cryptoHash.reset();
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    FileStore::ItemModifyJob *job = 0;
+
+    const QVariant colListVar = QVariant::fromValue<Collection::List>(Collection::List());
+    QVariant var;
+    Collection::List collections;
+    KMime::Message::Ptr msgPtr(new KMime::Message);
+
+    // test modifying a flag of a maildir items changes the entry name but not the
+    // message contents
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    // check that the \SEEN flag is not set yet
+    QVERIFY(!md1.readEntryFlags(entryList1.first()).contains("\\SEEN"));
+
+    const QByteArray data1 = md1.readEntry(entryList1.first());
+
+    msgPtr->setContent(KMime::CRLFtoLF(data1));
+    msgPtr->subject()->from7BitString("Modify Test");
+    msgPtr->assemble();
+
+    Item item1;
+    item1.setMimeType(KMime::Message::mimeType());
+    item1.setRemoteId(entryList1.first());
+    item1.setParentCollection(collection1);
+    item1.setPayload<KMime::Message::Ptr>(msgPtr);
+    item1.setFlag("\\SEEN");
+
+    job = mStore->modifyItem(item1);
+    // setting \SEEN, so indicate a flags change
+    job->setParts(QSet<QByteArray>() << "FLAGS");
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    Item item = job->item();
+
+    // returned item should contain the change
+    QVERIFY(item.flags().contains("\\SEEN"));
+
+    // remote ID has changed
+    QVERIFY(item.remoteId() != entryList1.first());
+    QVERIFY(!md1.entryList().contains(entryList1.first()));
+
+    // no change in number of entries, one difference
+    QStringList entryList3 = md1.entryList();
+    QCOMPARE(entryList3.count(), entryList1.count());
+    Q_FOREACH (const QString &oldEntry, entryList1) {
+        entryList3.removeAll(oldEntry);
+    }
+    QCOMPARE(entryList3.count(), 1);
+
+    // no change to data
+    QCOMPARE(md1.readEntry(entryList3.first()), data1);
+
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection1);
+
+    // fetch new item, check flag
+    item1 = Item();
+    item1.setMimeType(KMime::Message::mimeType());
+    item1.setRemoteId(entryList3.first());
+    item1.setParentCollection(collection1);
+
+    FileStore::ItemFetchJob *itemFetch = mStore->fetchItem(item1);
+
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    QCOMPARE(itemFetch->items().count(), 1);
+    QEXPECT_FAIL("", "ItemFetch handling needs to be fixed to also fetch flags", Continue);
+    QVERIFY(itemFetch->items()[ 0 ].flags().contains("\\SEEN"));
+
+    // test modifying flags of an mbox item "succeeds" (no error) but does not change
+    // anything in store or on disk
+    Collection collection2;
+    collection2.setName(QStringLiteral("collection2"));
+    collection2.setRemoteId(QStringLiteral("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+
+    const QByteArray data2 = mbox2.readRawMessage(MBoxEntry(0));
+
+    msgPtr->setContent(KMime::CRLFtoLF(data2));
+    msgPtr->subject()->from7BitString("Modify Test");
+    msgPtr->assemble();
+
+    Item item2;
+    item2.setMimeType(KMime::Message::mimeType());
+    item2.setRemoteId(QStringLiteral("0"));
+    item2.setParentCollection(collection2);
+    item2.setPayload<KMime::Message::Ptr>(msgPtr);
+    item2.setFlag("\\SEEN");
+
+    job = mStore->modifyItem(item2);
+    // setting \SEEN, so indicate a flags change
+    job->setParts(QSet<QByteArray>() << "FLAGS");
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    item = job->item();
+
+    // returned item should contain the change
+    QVERIFY(item.flags().contains("\\SEEN"));
+
+    // mbox not changed
+    QVERIFY(mbox2.load(mbox2.fileName()));
+    MBoxEntry::List entryList = mbox2.entries();
+    QCOMPARE((int)entryList.count(), 4);
+
+    var = job->property("compactStore");
+    QVERIFY(!var.isValid());
+
+    // file not modified
+    QVERIFY(file2.open(QIODevice::ReadOnly));
+    cryptoHash.addData(file2.readAll());
+    QCOMPARE(cryptoHash.result(), mbox2Sha1);
+
+    file2.close();
+    cryptoHash.reset();
+
+    // check index preservation is not triggered
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(!var.isValid());
+
+}
+
+void ItemModifyTest::testModifyFlagsAndPayload()
+{
+    QDir topDir(mDir->path());
+
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("maildir"), topDir.path(), QStringLiteral("collection1")));
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox"), topDir.path(), QStringLiteral("collection2")));
+
+    KPIM::Maildir topLevelMd(topDir.path(), true);
+
+    KPIM::Maildir md1 = topLevelMd.subFolder(QStringLiteral("collection1"));
+    QStringList entryList1 = md1.entryList();
+    QCOMPARE((int)entryList1.count(), 4);
+
+    QFileInfo fileInfo2(topDir.path(), QStringLiteral("collection2"));
+    MBox mbox2;
+    QVERIFY(mbox2.load(fileInfo2.absoluteFilePath()));
+    MBoxEntry::List entryList2 = mbox2.entries();
+    QCOMPARE((int)entryList2.count(), 4);
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    FileStore::ItemModifyJob *job = 0;
+
+    const QVariant colListVar = QVariant::fromValue<Collection::List>(Collection::List());
+    QVariant var;
+    Collection::List collections;
+    KMime::Message::Ptr msgPtr(new KMime::Message);
+
+    // test modifying a flag of a maildir items changes the entry name but not the
+    // message contents
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    // check that the \SEEN flag is not set yet
+    QVERIFY(!md1.readEntryFlags(entryList1.first()).contains("\\SEEN"));
+
+    const QByteArray data1 = md1.readEntry(entryList1.first());
+
+    msgPtr->setContent(KMime::CRLFtoLF(data1));
+    msgPtr->subject()->from7BitString("Modify Test");
+    msgPtr->assemble();
+
+    Item item1;
+    item1.setMimeType(KMime::Message::mimeType());
+    item1.setRemoteId(entryList1.first());
+    item1.setParentCollection(collection1);
+    item1.setPayload<KMime::Message::Ptr>(msgPtr);
+    item1.setFlag("\\SEEN");
+
+    job = mStore->modifyItem(item1);
+    // setting \SEEN so indicate a flags change and
+    // setting new subject so indicate a payload change
+    job->setParts(QSet<QByteArray>() << "FLAGS"
+                  << QByteArray("PLD:") + QByteArray(MessagePart::Header));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    Item item = job->item();
+
+    // returned item should contain the change
+    QVERIFY(item.flags().contains("\\SEEN"));
+
+    // remote ID has changed
+    QVERIFY(item.remoteId() != entryList1.first());
+    QVERIFY(!md1.entryList().contains(entryList1.first()));
+
+    // no change in number of entries, one difference
+    QStringList entryList3 = md1.entryList();
+    QCOMPARE(entryList3.count(), entryList1.count());
+    Q_FOREACH (const QString &oldEntry, entryList1) {
+        entryList3.removeAll(oldEntry);
+    }
+    QCOMPARE(entryList3.count(), 1);
+
+    // data changed
+    QCOMPARE(md1.readEntry(entryList3.first()), msgPtr->encodedContent());
+
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection1);
+
+    // fetch new item, check flag
+    item1 = Item();
+    item1.setMimeType(KMime::Message::mimeType());
+    item1.setRemoteId(entryList3.first());
+    item1.setParentCollection(collection1);
+
+    FileStore::ItemFetchJob *itemFetch = mStore->fetchItem(item1);
+    itemFetch->fetchScope().fetchFullPayload();
+
+    QVERIFY(itemFetch->exec());
+    QCOMPARE(itemFetch->error(), 0);
+
+    QCOMPARE(itemFetch->items().count(), 1);
+    Item fetchedItem = itemFetch->items().first();
+    QEXPECT_FAIL("", "ItemFetch handling needs to be fixed to also fetch flags", Continue);
+    QVERIFY(fetchedItem.flags().contains("\\SEEN"));
+
+    QVERIFY(fetchedItem.hasPayload<KMime::Message::Ptr>());
+    KMime::Message::Ptr fetchedMsgPtr = fetchedItem.payload<KMime::Message::Ptr>();
+    QCOMPARE(msgPtr->encodedContent(), fetchedMsgPtr->encodedContent());
+
+    // TODO test for mbox.
+}
+
+QTEST_MAIN(ItemModifyTest)
+
+#include "itemmodifytest.moc"
+
diff --git a/resources/mixedmaildir/autotests/itemmovetest.cpp b/resources/mixedmaildir/autotests/itemmovetest.cpp
new file mode 100644 (file)
index 0000000..0b538ac
--- /dev/null
@@ -0,0 +1,676 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "mixedmaildirstore.h"
+
+#include "testdatautil.h"
+
+#include "filestore/entitycompactchangeattribute.h"
+#include "filestore/itemfetchjob.h"
+#include "filestore/itemmovejob.h"
+#include "filestore/storecompactjob.h"
+
+#include "libmaildir/maildir.h"
+
+#include <kmbox/mbox.h>
+#include <kmime/kmime_message.h>
+
+#include <KRandom>
+#include <QTemporaryDir>
+
+#include <QSignalSpy>
+
+#include <qtest.h>
+#include <QFileInfo>
+#include <QDir>
+using namespace Akonadi;
+using namespace KMBox;
+
+static bool fullEntryCompare(const MBoxEntry &a, const MBoxEntry &b)
+{
+    return a.messageOffset() == b.messageOffset() &&
+           a.separatorSize() == b.separatorSize() &&
+           a.messageSize() == b.messageSize();
+}
+
+static quint64 changedOffset(const Item &item)
+{
+    Q_ASSERT(item.hasAttribute<FileStore::EntityCompactChangeAttribute>());
+
+    const QString remoteId = item.attribute<FileStore::EntityCompactChangeAttribute>()->remoteId();
+    Q_ASSERT(!remoteId.isEmpty());
+
+    bool ok = false;
+    const quint64 result = remoteId.toULongLong(&ok);
+    Q_ASSERT(ok);
+
+    return result;
+}
+
+class ItemMoveTest : public QObject
+{
+    Q_OBJECT
+
+public:
+    ItemMoveTest()
+        : QObject(), mStore(0), mDir(0) {}
+
+    ~ItemMoveTest()
+    {
+        delete mStore;
+        delete mDir;
+    }
+
+private:
+    MixedMaildirStore *mStore;
+    QTemporaryDir *mDir;
+
+private Q_SLOTS:
+    void init();
+    void cleanup();
+    void testExpectedFail();
+    void testMaildirItem();
+    void testMBoxItem();
+};
+
+void ItemMoveTest::init()
+{
+    mStore = new MixedMaildirStore;
+
+    mDir = new QTemporaryDir;
+    QVERIFY(mDir->isValid());
+    QVERIFY(QDir(mDir->path()).exists());
+}
+
+void ItemMoveTest::cleanup()
+{
+    delete mStore;
+    mStore = 0;
+    delete mDir;
+    mDir = 0;
+}
+
+void ItemMoveTest::testExpectedFail()
+{
+    QDir topDir(mDir->path());
+
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), topDir.path(), QStringLiteral("collection1")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), topDir.path(), QStringLiteral("collection2")));
+
+    KPIM::Maildir topLevelMd(topDir.path(), true);
+
+    KPIM::Maildir md1 = topLevelMd.subFolder(QStringLiteral("collection1"));
+    QSet<QString> entrySet1 = QSet<QString>::fromList(md1.entryList());
+    QCOMPARE((int)entrySet1.count(), 4);
+
+    QFileInfo fileInfo2(topDir.path(), QStringLiteral("collection2"));
+    MBox mbox2;
+    QVERIFY(mbox2.load(fileInfo2.absoluteFilePath()));
+    MBoxEntry::List entryList2 = mbox2.entries();
+    QCOMPARE((int)entryList2.count(), 4);
+
+    QSet<qint64> entrySet2;
+    Q_FOREACH (const MBoxEntry &entry, entryList2) {
+        entrySet2 << entry.messageOffset();
+    }
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    FileStore::ItemMoveJob *job = 0;
+
+    // test failure of moving from a non-existent collection
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    Collection collection3;
+    collection3.setName(QStringLiteral("collection3"));
+    collection3.setRemoteId(QStringLiteral("collection3"));
+    collection3.setParentCollection(mStore->topLevelCollection());
+
+    Item item3;
+    item3.setRemoteId(QStringLiteral("item3"));
+    item3.setParentCollection(collection3);
+
+    job = mStore->moveItem(item3, collection1);
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+
+    QCOMPARE(QSet<QString>::fromList(md1.entryList()), entrySet1);
+
+    // test failure of moving from maildir to non-existent collection
+    Item item1;
+    item1.setId(KRandom::random());
+    item1.setRemoteId(*entrySet1.cbegin());
+    item1.setParentCollection(collection1);
+
+    job = mStore->moveItem(item1, collection3);
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+
+    QCOMPARE(QSet<QString>::fromList(md1.entryList()), entrySet1);
+
+    // test failure of moving from mbox to non-existent collection
+    Collection collection2;
+    collection2.setName(QStringLiteral("collection2"));
+    collection2.setRemoteId(QStringLiteral("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+
+    Item item2;
+    item2.setId(KRandom::random());
+    item2.setRemoteId(QStringLiteral("0"));
+    item2.setParentCollection(collection2);
+
+    job = mStore->moveItem(item1, collection3);
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+
+    QVERIFY(mbox2.load(fileInfo2.absoluteFilePath()));
+    MBoxEntry::List tmpEntryList = mbox2.entries();
+    QVERIFY(std::equal(tmpEntryList.begin(), tmpEntryList.end(), entryList2.begin(), fullEntryCompare));
+
+    // test failure of moving from maildir to top level collection
+    job = mStore->moveItem(item1, mStore->topLevelCollection());
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+
+    QCOMPARE(QSet<QString>::fromList(md1.entryList()), entrySet1);
+
+    // test failure of moving from mbox to top level collection
+    job = mStore->moveItem(item1, mStore->topLevelCollection());
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+
+    QVERIFY(mbox2.load(fileInfo2.absoluteFilePath()));
+    tmpEntryList = mbox2.entries();
+    QVERIFY(std::equal(tmpEntryList.begin(), tmpEntryList.end(), entryList2.begin(), fullEntryCompare));
+
+    // test failure of moving a non-existent maildir entry
+    QString remoteId1;
+    do {
+        remoteId1 = KRandom::randomString(20);
+    } while (entrySet1.contains(remoteId1));
+
+    item1.setRemoteId(remoteId1);
+
+    job = mStore->moveItem(item1, collection2);
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+
+    QCOMPARE(QSet<QString>::fromList(md1.entryList()), entrySet1);
+    QVERIFY(mbox2.load(fileInfo2.absoluteFilePath()));
+    tmpEntryList = mbox2.entries();
+    QVERIFY(std::equal(tmpEntryList.begin(), tmpEntryList.end(), entryList2.begin(), fullEntryCompare));
+
+    // test failure of moving a non-existent mbox entry
+    quint64 remoteId2;
+    do {
+        remoteId2 = KRandom::random();
+    } while (entrySet2.contains(remoteId2));
+
+    item2.setRemoteId(QString::number(remoteId2));
+
+    job = mStore->moveItem(item2, collection1);
+    QVERIFY(!job->exec());
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+
+    QCOMPARE(QSet<QString>::fromList(md1.entryList()), entrySet1);
+    QVERIFY(mbox2.load(fileInfo2.absoluteFilePath()));
+    tmpEntryList = mbox2.entries();
+    QVERIFY(std::equal(tmpEntryList.begin(), tmpEntryList.end(), entryList2.begin(), fullEntryCompare));
+}
+
+void ItemMoveTest::testMaildirItem()
+{
+    QDir topDir(mDir->path());
+
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), topDir.path(), QStringLiteral("collection1")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), topDir.path(), QStringLiteral("collection2")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), topDir.path(), QStringLiteral("collection5")));
+
+    KPIM::Maildir topLevelMd(topDir.path(), true);
+
+    KPIM::Maildir md1 = topLevelMd.subFolder(QStringLiteral("collection1"));
+    QSet<QString> entrySet1 = QSet<QString>::fromList(md1.entryList());
+    QCOMPARE((int)entrySet1.count(), 4);
+
+    QFileInfo fileInfo2(topDir.path(), QStringLiteral("collection2"));
+    MBox mbox2;
+    QVERIFY(mbox2.load(fileInfo2.absoluteFilePath()));
+    MBoxEntry::List entryList2 = mbox2.entries();
+    QCOMPARE((int)entryList2.count(), 4);
+
+    KPIM::Maildir md3(topLevelMd.addSubFolder(QStringLiteral("collection3")), false);
+    QVERIFY(md3.isValid());
+    QSet<QString> entrySet3 = QSet<QString>::fromList(md3.entryList());
+    QCOMPARE((int)entrySet3.count(), 0);
+
+    QFileInfo fileInfo4(topDir.path(), QStringLiteral("collection4"));
+    QFile file4(fileInfo4.absoluteFilePath());
+    QVERIFY(file4.open(QIODevice::WriteOnly));
+    file4.close();
+    fileInfo4.refresh();
+    QVERIFY(fileInfo4.exists());
+    MBox mbox4;
+    QVERIFY(mbox4.load(fileInfo4.absoluteFilePath()));
+    MBoxEntry::List entryList4 = mbox4.entries();
+    QCOMPARE((int)entryList4.count(), 0);
+
+    KPIM::Maildir md5 = topLevelMd.subFolder(QStringLiteral("collection5"));
+    QSet<QString> entrySet5 = QSet<QString>::fromList(md5.entryList());
+    QCOMPARE((int)entrySet5.count(), 4);
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    FileStore::ItemMoveJob *job = 0;
+
+    const QVariant colListVar = QVariant::fromValue<Collection::List>(Collection::List());
+    QVariant var;
+    Collection::List collections;
+    Item movedItem;
+    MBoxEntry::List entryList;
+
+    // test moving to an empty maildir
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    Collection collection3;
+    collection3.setName(QStringLiteral("collection3"));
+    collection3.setRemoteId(QStringLiteral("collection3"));
+    collection3.setParentCollection(mStore->topLevelCollection());
+
+    Item item1;
+    item1.setId(KRandom::random());
+    item1.setRemoteId(*entrySet1.cbegin());
+    item1.setParentCollection(collection1);
+
+    job = mStore->moveItem(item1, collection3);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    movedItem = job->item();
+    QCOMPARE(movedItem.id(), item1.id());
+    QCOMPARE(movedItem.parentCollection(), collection3);
+
+    entrySet3 << movedItem.remoteId();
+    QCOMPARE(QSet<QString>::fromList(md3.entryList()), entrySet3);
+    entrySet1.remove(item1.remoteId());
+    QCOMPARE(QSet<QString>::fromList(md1.entryList()), entrySet1);
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection1);
+
+    // test moving to a non empty maildir
+    item1.setRemoteId(*entrySet1.cbegin());
+
+    Collection collection5;
+    collection5.setName(QStringLiteral("collection5"));
+    collection5.setRemoteId(QStringLiteral("collection5"));
+    collection5.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->moveItem(item1, collection5);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    movedItem = job->item();
+    QCOMPARE(movedItem.id(), item1.id());
+    QCOMPARE(movedItem.parentCollection(), collection5);
+
+    entrySet5 << movedItem.remoteId();
+    QCOMPARE(QSet<QString>::fromList(md5.entryList()), entrySet5);
+    entrySet1.remove(item1.remoteId());
+    QCOMPARE(QSet<QString>::fromList(md1.entryList()), entrySet1);
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 2);
+    QCOMPARE(collections, Collection::List() << collection1 << collection5);
+
+    // test moving to an empty mbox
+    Collection collection4;
+    collection4.setName(QStringLiteral("collection4"));
+    collection4.setRemoteId(QStringLiteral("collection4"));
+    collection4.setParentCollection(mStore->topLevelCollection());
+
+    item1.setRemoteId(*entrySet1.cbegin());
+
+    job = mStore->moveItem(item1, collection4);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    movedItem = job->item();
+    QCOMPARE(movedItem.id(), item1.id());
+    QCOMPARE(movedItem.parentCollection(), collection4);
+
+    QVERIFY(mbox4.load(mbox4.fileName()));
+    entryList = mbox4.entries();
+    QCOMPARE((int)entryList.count(), 1);
+
+    QCOMPARE(entryList.last().messageOffset(), movedItem.remoteId().toULongLong());
+    entrySet1.remove(item1.remoteId());
+    QCOMPARE(QSet<QString>::fromList(md1.entryList()), entrySet1);
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection1);
+
+    // test moving to a non empty mbox
+    item1.setRemoteId(*entrySet1.cbegin());
+
+    Collection collection2;
+    collection2.setName(QStringLiteral("collection2"));
+    collection2.setRemoteId(QStringLiteral("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->moveItem(item1, collection2);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    movedItem = job->item();
+    QCOMPARE(movedItem.id(), item1.id());
+    QCOMPARE(movedItem.parentCollection(), collection2);
+
+    QVERIFY(mbox2.load(mbox2.fileName()));
+    entryList = mbox2.entries();
+    QCOMPARE((int)entryList.count(), 5);
+
+    QCOMPARE(entryList.last().messageOffset(), movedItem.remoteId().toULongLong());
+    entrySet1.remove(item1.remoteId());
+    QCOMPARE(QSet<QString>::fromList(md1.entryList()), entrySet1);
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 2);
+    QCOMPARE(collections, Collection::List() << collection1 << collection2);
+}
+
+void ItemMoveTest::testMBoxItem()
+{
+    QDir topDir(mDir->path());
+
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), topDir.path(), QStringLiteral("collection1")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("maildir"), topDir.path(), QStringLiteral("collection2")));
+    QVERIFY(TestDataUtil::installFolder(QStringLiteral("mbox"), topDir.path(), QStringLiteral("collection5")));
+
+    QFileInfo fileInfo1(topDir.path(), QStringLiteral("collection1"));
+    MBox mbox1;
+    QVERIFY(mbox1.load(fileInfo1.absoluteFilePath()));
+    MBoxEntry::List entryList1 = mbox1.entries();
+    QCOMPARE((int)entryList1.count(), 4);
+
+    KPIM::Maildir topLevelMd(topDir.path(), true);
+
+    KPIM::Maildir md2 = topLevelMd.subFolder(QStringLiteral("collection2"));
+    QSet<QString> entrySet2 = QSet<QString>::fromList(md2.entryList());
+    QCOMPARE((int)entrySet2.count(), 4);
+
+    KPIM::Maildir md3(topLevelMd.addSubFolder(QStringLiteral("collection3")), false);
+    QVERIFY(md3.isValid());
+    QSet<QString> entrySet3 = QSet<QString>::fromList(md3.entryList());
+    QCOMPARE((int)entrySet3.count(), 0);
+
+    QFileInfo fileInfo4(topDir.path(), QStringLiteral("collection4"));
+    QFile file4(fileInfo4.absoluteFilePath());
+    QVERIFY(file4.open(QIODevice::WriteOnly));
+    file4.close();
+    fileInfo4.refresh();
+    QVERIFY(fileInfo4.exists());
+    MBox mbox4;
+    QVERIFY(mbox4.load(fileInfo4.absoluteFilePath()));
+    MBoxEntry::List entryList4 = mbox4.entries();
+    QCOMPARE((int)entryList4.count(), 0);
+
+    QFileInfo fileInfo5(topDir.path(), QStringLiteral("collection5"));
+    MBox mbox5;
+    QVERIFY(mbox5.load(fileInfo5.absoluteFilePath()));
+    MBoxEntry::List entryList5 = mbox5.entries();
+    QCOMPARE((int)entryList5.count(), 4);
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    FileStore::ItemMoveJob *job = 0;
+    FileStore::StoreCompactJob *compactStore = 0;
+
+    const QVariant colListVar = QVariant::fromValue<Collection::List>(Collection::List());
+    QVariant var;
+    Collection::List collections;
+    Item movedItem;
+    MBoxEntry::List entryList;
+    Item::List items;
+
+    // test moving to an empty maildir
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    Collection collection3;
+    collection3.setName(QStringLiteral("collection3"));
+    collection3.setRemoteId(QStringLiteral("collection3"));
+    collection3.setParentCollection(mStore->topLevelCollection());
+
+    Item item1;
+    item1.setId(KRandom::random());
+    item1.setRemoteId(QString::number(entryList1.first().messageOffset()));
+    item1.setParentCollection(collection1);
+
+    job = mStore->moveItem(item1, collection3);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    movedItem = job->item();
+    QCOMPARE(movedItem.id(), item1.id());
+    QCOMPARE(movedItem.parentCollection(), collection3);
+
+    var = job->property("compactStore");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.type(), QVariant::Bool);
+    QCOMPARE(var.toBool(), true);
+
+    compactStore = mStore->compactStore();
+    QVERIFY(compactStore->exec());
+
+    items = compactStore->changedItems();
+    QCOMPARE((int)items.count(), 3);
+
+    entrySet3 << movedItem.remoteId();
+    QCOMPARE(QSet<QString>::fromList(md3.entryList()), entrySet3);
+
+    entryList1.removeAt(0);
+    entryList1[ 0 ] = MBoxEntry(changedOffset(items[ 0 ]));
+    entryList1[ 1 ] = MBoxEntry(changedOffset(items[ 1 ]));
+    entryList1[ 2 ] = MBoxEntry(changedOffset(items[ 2 ]));
+    QVERIFY(mbox1.load(mbox1.fileName()));
+    QCOMPARE(mbox1.entries(), entryList1);
+
+    // test moving to a non empty mbox
+    Collection collection5;
+    collection5.setName(QStringLiteral("collection5"));
+    collection5.setRemoteId(QStringLiteral("collection5"));
+    collection5.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->moveItem(item1, collection5);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    movedItem = job->item();
+    QCOMPARE(movedItem.id(), item1.id());
+    QCOMPARE(movedItem.parentCollection(), collection5);
+
+    var = job->property("compactStore");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.type(), QVariant::Bool);
+    QCOMPARE(var.toBool(), true);
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 2);
+    QCOMPARE(collections, Collection::List() << collection1 << collection5);
+
+    compactStore = mStore->compactStore();
+    QVERIFY(compactStore->exec());
+
+    items = compactStore->changedItems();
+    QCOMPARE((int)items.count(), 2);
+
+    QVERIFY(mbox5.load(mbox5.fileName()));
+    QCOMPARE(mbox5.entries().count(), entryList5.count() + 1);
+    QCOMPARE(mbox5.entries().last().messageOffset(), movedItem.remoteId().toULongLong());
+
+    entryList1.removeAt(0);
+    entryList1[ 0 ] = MBoxEntry(changedOffset(items[ 0 ]));
+    entryList1[ 1 ] = MBoxEntry(changedOffset(items[ 1 ]));
+    QVERIFY(mbox1.load(mbox1.fileName()));
+    QCOMPARE(mbox1.entries(), entryList1);
+
+    // test moving to an empty mbox
+    Collection collection4;
+    collection4.setName(QStringLiteral("collection4"));
+    collection4.setRemoteId(QStringLiteral("collection4"));
+    collection4.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->moveItem(item1, collection4);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    movedItem = job->item();
+    QCOMPARE(movedItem.id(), item1.id());
+    QCOMPARE(movedItem.parentCollection(), collection4);
+
+    QVERIFY(mbox4.load(mbox4.fileName()));
+    entryList = mbox4.entries();
+    QCOMPARE((int)entryList.count(), 1);
+
+    QCOMPARE(entryList.last().messageOffset(), movedItem.remoteId().toULongLong());
+
+    var = job->property("compactStore");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.type(), QVariant::Bool);
+    QCOMPARE(var.toBool(), true);
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections.first(), collection1);
+
+    compactStore = mStore->compactStore();
+    QVERIFY(compactStore->exec());
+
+    items = compactStore->changedItems();
+    QCOMPARE((int)items.count(), 1);
+
+    QCOMPARE(mbox4.entries().count(), entryList4.count() + 1);
+    QCOMPARE(mbox4.entries().last().messageOffset(), movedItem.remoteId().toULongLong());
+
+    entryList1.removeAt(0);
+    entryList1[ 0 ] = MBoxEntry(changedOffset(items[ 0 ]));
+    QVERIFY(mbox1.load(mbox1.fileName()));
+    QCOMPARE(mbox1.entries(), entryList1);
+
+    // test moving to a non empty maildir
+    Collection collection2;
+    collection2.setName(QStringLiteral("collection2"));
+    collection2.setRemoteId(QStringLiteral("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+
+    job = mStore->moveItem(item1, collection2);
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    movedItem = job->item();
+    QCOMPARE(movedItem.id(), item1.id());
+    QCOMPARE(movedItem.parentCollection(), collection2);
+
+    QSet<QString> entrySet = QSet<QString>::fromList(md2.entryList());
+    QCOMPARE((int)entrySet.count(), 5);
+
+    QVERIFY(entrySet.contains(movedItem.remoteId()));
+
+    var = job->property("compactStore");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.type(), QVariant::Bool);
+    QCOMPARE(var.toBool(), true);
+
+    // check for index preservation
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 2);
+    QCOMPARE(collections.first(), collection1);
+
+    compactStore = mStore->compactStore();
+    QVERIFY(compactStore->exec());
+
+    items = compactStore->changedItems();
+    QCOMPARE((int)items.count(), 0);
+
+    entryList1.removeAt(0);
+    QVERIFY(mbox1.load(mbox1.fileName()));
+    const MBoxEntry::List newEntryList = mbox1.entries();
+    QVERIFY(std::equal(newEntryList.begin(), newEntryList.end(), entryList1.begin(), fullEntryCompare));
+}
+
+QTEST_MAIN(ItemMoveTest)
+
+#include "itemmovetest.moc"
+
diff --git a/resources/mixedmaildir/autotests/storecompacttest.cpp b/resources/mixedmaildir/autotests/storecompacttest.cpp
new file mode 100644 (file)
index 0000000..3daae07
--- /dev/null
@@ -0,0 +1,409 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "mixedmaildirstore.h"
+
+#include "testdatautil.h"
+
+#include "filestore/entitycompactchangeattribute.h"
+#include "filestore/itemdeletejob.h"
+#include "filestore/storecompactjob.h"
+
+#include <kmbox/mbox.h>
+
+#include <KRandom>
+#include <QTemporaryDir>
+
+#include <QSignalSpy>
+
+#include <qtest.h>
+#include <QFileInfo>
+#include <QDir>
+using namespace Akonadi;
+using namespace KMBox;
+
+static Collection::List collectionsFromSpy(QSignalSpy *spy)
+{
+    Collection::List collections;
+
+    QListIterator<QList<QVariant> > it(*spy);
+    while (it.hasNext()) {
+        const QList<QVariant> invocation = it.next();
+        Q_ASSERT(invocation.count() == 1);
+
+        collections << invocation.first().value<Collection::List>();
+    }
+
+    return collections;
+}
+
+static Item::List itemsFromSpy(QSignalSpy *spy)
+{
+    Item::List items;
+
+    QListIterator<QList<QVariant> > it(*spy);
+    while (it.hasNext()) {
+        const QList<QVariant> invocation = it.next();
+        Q_ASSERT(invocation.count() == 1);
+
+        items << invocation.first().value<Item::List>();
+    }
+
+    return items;
+}
+
+static bool fullEntryCompare(const MBoxEntry &a, const MBoxEntry &b)
+{
+    return a.messageOffset() == b.messageOffset() &&
+           a.separatorSize() == b.separatorSize() &&
+           a.messageSize() == b.messageSize();
+}
+
+static quint64 changedOffset(const Item &item)
+{
+    Q_ASSERT(item.hasAttribute<FileStore::EntityCompactChangeAttribute>());
+
+    const QString remoteId = item.attribute<FileStore::EntityCompactChangeAttribute>()->remoteId();
+    Q_ASSERT(!remoteId.isEmpty());
+
+    bool ok = false;
+    const quint64 result = remoteId.toULongLong(&ok);
+    Q_ASSERT(ok);
+
+    return result;
+}
+
+class StoreCompactTest : public QObject
+{
+    Q_OBJECT
+
+public:
+    StoreCompactTest() : QObject(), mStore(0), mDir(0)
+    {
+        // for monitoring signals
+        qRegisterMetaType<Akonadi::Collection::List>();
+        qRegisterMetaType<Akonadi::Item::List>();
+    }
+
+    ~StoreCompactTest()
+    {
+        delete mStore;
+        delete mDir;
+    }
+
+private:
+    MixedMaildirStore *mStore;
+    QTemporaryDir *mDir;
+
+private Q_SLOTS:
+    void init();
+    void cleanup();
+    void testCompact();
+};
+
+void StoreCompactTest::init()
+{
+    mStore = new MixedMaildirStore;
+
+    mDir = new QTemporaryDir;
+    QVERIFY(mDir->isValid());
+    QVERIFY(QDir(mDir->path()).exists());
+}
+
+void StoreCompactTest::cleanup()
+{
+    delete mStore;
+    mStore = 0;
+    delete mDir;
+    mDir = 0;
+}
+
+void StoreCompactTest::testCompact()
+{
+    QDir topDir(mDir->path());
+
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox"), topDir.path(), QStringLiteral("collection1")));
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox"), topDir.path(), QStringLiteral("collection2")));
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox"), topDir.path(), QStringLiteral("collection3")));
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox"), topDir.path(), QStringLiteral("collection4")));
+
+    QFileInfo fileInfo1(topDir.path(), QStringLiteral("collection1"));
+    MBox mbox1;
+    QVERIFY(mbox1.load(fileInfo1.absoluteFilePath()));
+    MBoxEntry::List entryList1 = mbox1.entries();
+    QCOMPARE((int)entryList1.count(), 4);
+
+    QFileInfo fileInfo2(topDir.path(), QStringLiteral("collection2"));
+    MBox mbox2;
+    QVERIFY(mbox2.load(fileInfo2.absoluteFilePath()));
+    MBoxEntry::List entryList2 = mbox2.entries();
+    QCOMPARE((int)entryList2.count(), 4);
+
+    QFileInfo fileInfo3(topDir.path(), QStringLiteral("collection3"));
+    MBox mbox3;
+    QVERIFY(mbox3.load(fileInfo3.absoluteFilePath()));
+    MBoxEntry::List entryList3 = mbox3.entries();
+    QCOMPARE((int)entryList3.count(), 4);
+
+    QFileInfo fileInfo4(topDir.path(), QStringLiteral("collection4"));
+    MBox mbox4;
+    QVERIFY(mbox4.load(fileInfo4.absoluteFilePath()));
+    MBoxEntry::List entryList4 = mbox4.entries();
+    QCOMPARE((int)entryList4.count(), 4);
+
+    mStore->setPath(topDir.path());
+
+    // common variables
+    FileStore::CollectionFetchJob *collectionFetch = 0;
+    FileStore::ItemDeleteJob *itemDelete = 0;
+    FileStore::StoreCompactJob *job = 0;
+
+    Collection::List collections;
+    Item::List items;
+
+    QSignalSpy *collectionSpy = 0;
+    QSignalSpy *itemSpy = 0;
+
+    MBoxEntry::List entryList;
+    Collection collection;
+    FileStore::EntityCompactChangeAttribute *attribute = 0;
+
+    const QVariant colListVar = QVariant::fromValue<Collection::List>(Collection::List());
+    QVariant var;
+
+    // test compact after delete from the end of an mbox
+    Collection collection1;
+    collection1.setName(QStringLiteral("collection1"));
+    collection1.setRemoteId(QStringLiteral("collection1"));
+    collection1.setParentCollection(mStore->topLevelCollection());
+
+    Item item1;
+    item1.setRemoteId(QString::number(entryList1.last().messageOffset()));
+    item1.setParentCollection(collection1);
+
+    itemDelete = mStore->deleteItem(item1);
+
+    QVERIFY(itemDelete->exec());
+
+    job = mStore->compactStore();
+
+    collectionSpy = new QSignalSpy(job, SIGNAL(collectionsChanged(Akonadi::Collection::List)));
+    itemSpy = new QSignalSpy(job, SIGNAL(itemsChanged(Akonadi::Item::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collections = job->changedCollections();
+    items = job->changedItems();
+
+    QCOMPARE(collections.count(), 0);
+    QCOMPARE(items.count(), 0);
+
+    QCOMPARE(collectionSpy->count(), 0);
+    QCOMPARE(itemSpy->count(), 0);
+
+    QVERIFY(mbox1.load(mbox1.fileName()));
+    entryList = mbox1.entries();
+    entryList1.pop_back();
+    QVERIFY(std::equal(entryList.begin(), entryList.end(), entryList1.begin(), fullEntryCompare));
+
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections, Collection::List() << collection1);
+
+    // test compact after delete from before the end of an mbox
+    Collection collection2;
+    collection2.setName(QStringLiteral("collection2"));
+    collection2.setRemoteId(QStringLiteral("collection2"));
+    collection2.setParentCollection(mStore->topLevelCollection());
+
+    Item item2;
+    item2.setRemoteId(QString::number(entryList2.first().messageOffset()));
+    item2.setParentCollection(collection2);
+
+    itemDelete = mStore->deleteItem(item2);
+
+    QVERIFY(itemDelete->exec());
+
+    job = mStore->compactStore();
+
+    collectionSpy = new QSignalSpy(job, SIGNAL(collectionsChanged(Akonadi::Collection::List)));
+    itemSpy = new QSignalSpy(job, SIGNAL(itemsChanged(Akonadi::Item::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collections = job->changedCollections();
+    items = job->changedItems();
+
+    QCOMPARE(collections.count(), 1);
+    QCOMPARE(items.count(), 3);
+
+    QCOMPARE(collectionSpy->count(), 1);
+    QCOMPARE(itemSpy->count(), 1);
+
+    QCOMPARE(collectionsFromSpy(collectionSpy), collections);
+    QCOMPARE(itemsFromSpy(itemSpy), items);
+
+    collection = collections.first();
+    QCOMPARE(collection, collection2);
+    QVERIFY(collection.hasAttribute<FileStore::EntityCompactChangeAttribute>());
+    attribute = collection.attribute<FileStore::EntityCompactChangeAttribute>();
+    QCOMPARE(attribute->remoteRevision().toInt(), collection2.remoteRevision().toInt() + 1);
+
+    QVERIFY(mbox2.load(mbox2.fileName()));
+    entryList = mbox2.entries();
+
+    entryList2.pop_front();
+    for (int i = 0; i < items.count(); ++i) {
+        entryList2[ i ] = MBoxEntry(changedOffset(items[ i ]));
+    }
+    QCOMPARE(entryList, entryList2);
+
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 1);
+    QCOMPARE(collections, Collection::List() << collection2);
+
+    collectionFetch = mStore->fetchCollections(collection2, FileStore::CollectionFetchJob::Base);
+
+    QVERIFY(collectionFetch->exec());
+
+    collections = collectionFetch->collections();
+    QCOMPARE((int)collections.count(), 1);
+
+    collection = collections.first();
+    QCOMPARE(collection, collection2);
+    QCOMPARE(collection.remoteRevision(), attribute->remoteRevision());
+
+    // test compact after delete from before the end of more than one mbox
+    Collection collection3;
+    collection3.setName(QStringLiteral("collection3"));
+    collection3.setRemoteId(QStringLiteral("collection3"));
+    collection3.setParentCollection(mStore->topLevelCollection());
+
+    Item item3;
+    item3.setRemoteId(QString::number(entryList3.first().messageOffset()));
+    item3.setParentCollection(collection3);
+
+    itemDelete = mStore->deleteItem(item3);
+
+    QVERIFY(itemDelete->exec());
+
+    Collection collection4;
+    collection4.setName(QStringLiteral("collection4"));
+    collection4.setRemoteId(QStringLiteral("collection4"));
+    collection4.setParentCollection(mStore->topLevelCollection());
+
+    Item item4;
+    item4.setRemoteId(QString::number(entryList3.value(1).messageOffset()));
+    item4.setParentCollection(collection4);
+
+    itemDelete = mStore->deleteItem(item4);
+
+    QVERIFY(itemDelete->exec());
+
+    job = mStore->compactStore();
+
+    collectionSpy = new QSignalSpy(job, SIGNAL(collectionsChanged(Akonadi::Collection::List)));
+    itemSpy = new QSignalSpy(job, SIGNAL(itemsChanged(Akonadi::Item::List)));
+
+    QVERIFY(job->exec());
+    QCOMPARE(job->error(), 0);
+
+    collections = job->changedCollections();
+    items = job->changedItems();
+
+    QCOMPARE(collections.count(), 2);
+    QCOMPARE(items.count(), 5);
+
+    QCOMPARE(collectionSpy->count(), 2);
+    QCOMPARE(itemSpy->count(), 2);
+
+    QCOMPARE(collectionsFromSpy(collectionSpy), collections);
+    QCOMPARE(itemsFromSpy(itemSpy), items);
+
+    QHash<QString, Collection> compactedCollections;
+    Q_FOREACH (const Collection &col, collections) {
+        compactedCollections.insert(col.remoteId(), col);
+    }
+    QCOMPARE(compactedCollections.count(), 2);
+
+    QVERIFY(compactedCollections.contains(collection3.remoteId()));
+    collection = compactedCollections[ collection3.remoteId() ];
+    QCOMPARE(collection, collection3);
+    QVERIFY(collection.hasAttribute<FileStore::EntityCompactChangeAttribute>());
+    attribute = collection.attribute<FileStore::EntityCompactChangeAttribute>();
+    QCOMPARE(attribute->remoteRevision().toInt(), collection3.remoteRevision().toInt() + 1);
+
+    QVERIFY(mbox3.load(mbox3.fileName()));
+    entryList = mbox3.entries();
+
+    // The order of items depends on the order of iteration of a QHash in MixedMaildirStore.
+    // This makes sure that the items are always sorted by collection and offset
+    qSort(items.begin(), items.end(),
+    [](const Akonadi::Item & left, const Akonadi::Item & right) {
+        return left.parentCollection().remoteId().compare(right.parentCollection().remoteId()) < 0 ||
+               (left.parentCollection().remoteId() == right.parentCollection().remoteId() && changedOffset(left) < changedOffset(right));
+    });
+
+    entryList3.pop_front();
+    for (int i = 0; i < entryList3.count(); ++i) {
+        entryList3[ i ] = MBoxEntry(changedOffset(items.first()));
+        items.pop_front();
+    }
+    QCOMPARE(entryList, entryList3);
+
+    QVERIFY(compactedCollections.contains(collection4.remoteId()));
+    collection = compactedCollections[ collection4.remoteId() ];
+    QCOMPARE(collection, collection4);
+    QVERIFY(collection.hasAttribute<FileStore::EntityCompactChangeAttribute>());
+    attribute = collection.attribute<FileStore::EntityCompactChangeAttribute>();
+    QCOMPARE(attribute->remoteRevision().toInt(), collection4.remoteRevision().toInt() + 1);
+
+    QVERIFY(mbox4.load(mbox4.fileName()));
+    entryList = mbox4.entries();
+
+    entryList4.removeAt(1);
+    for (int i = 0; i < items.count(); ++i) {
+        entryList4[ i + 1 ] = MBoxEntry(changedOffset(items[ i ]));
+    }
+    QCOMPARE(entryList, entryList4);
+
+    var = job->property("onDiskIndexInvalidated");
+    QVERIFY(var.isValid());
+    QCOMPARE(var.userType(), colListVar.userType());
+
+    collections = var.value<Collection::List>();
+    QCOMPARE((int)collections.count(), 2);
+    QCOMPARE(collections, Collection::List() << collection3 << collection4);
+}
+
+QTEST_MAIN(StoreCompactTest)
+
+#include "storecompacttest.moc"
+
diff --git a/resources/mixedmaildir/autotests/templatemethodstest.cpp b/resources/mixedmaildir/autotests/templatemethodstest.cpp
new file mode 100644 (file)
index 0000000..9475cab
--- /dev/null
@@ -0,0 +1,235 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "mixedmaildirstore.h"
+
+#include "collectioncreatejob.h"
+#include "collectiondeletejob.h"
+#include "collectionfetchjob.h"
+#include "collectionmodifyjob.h"
+#include "collectionmovejob.h"
+#include "itemcreatejob.h"
+#include "itemdeletejob.h"
+#include "itemfetchjob.h"
+#include "itemmodifyjob.h"
+#include "itemmovejob.h"
+#include "sessionimpls_p.h"
+#include "storecompactjob.h"
+
+#include <cachepolicy.h>
+#include <akonadi/kmime/messageparts.h>
+
+#include <kmime/kmime_message.h>
+
+#include <KRandom>
+#include <QTemporaryDir>
+
+#include <qtest.h>
+
+using namespace Akonadi;
+
+class TestStore : public MixedMaildirStore
+{
+    Q_OBJECT
+
+public:
+    TestStore() : mLastCheckedJob(0), mErrorCode(0) {}
+
+public:
+    Collection mTopLevelCollection;
+
+    mutable FileStore::Job *mLastCheckedJob;
+    mutable int mErrorCode;
+    mutable QString mErrorText;
+
+protected:
+    void setTopLevelCollection(const Collection &collection)
+    {
+        MixedMaildirStore::setTopLevelCollection(collection);
+    }
+
+    void checkCollectionMove(FileStore::CollectionMoveJob *job, int &errorCode, QString &errorText) const
+    {
+        MixedMaildirStore::checkCollectionMove(job, errorCode, errorText);
+
+        mLastCheckedJob = job;
+        mErrorCode = errorCode;
+        mErrorText = errorText;
+    }
+
+    void checkItemCreate(FileStore::ItemCreateJob *job, int &errorCode, QString &errorText) const
+    {
+        MixedMaildirStore::checkItemCreate(job, errorCode, errorText);
+
+        mLastCheckedJob = job;
+        mErrorCode = errorCode;
+        mErrorText = errorText;
+    }
+};
+
+class TemplateMethodsTest : public QObject
+{
+    Q_OBJECT
+
+public:
+    TemplateMethodsTest() : QObject(), mStore(0) {}
+    ~TemplateMethodsTest()
+    {
+        delete mStore;
+    }
+
+private:
+    QTemporaryDir mDir;
+    TestStore *mStore;
+
+private Q_SLOTS:
+    void init();
+    void testSetTopLevelCollection();
+    void testMoveCollection();
+    void testCreateItem();
+};
+
+void TemplateMethodsTest::init()
+{
+    delete mStore;
+    mStore = new TestStore;
+    QVERIFY(mDir.isValid());
+    QVERIFY(QDir(mDir.path()).exists());
+}
+
+void TemplateMethodsTest::testSetTopLevelCollection()
+{
+    const QString file = KRandom::randomString(10);
+    const QString path = mDir.path() + file;
+
+    mStore->setPath(path);
+
+    // check the adjustments on the top level collection
+    const Collection collection = mStore->topLevelCollection();
+    QCOMPARE(collection.contentMimeTypes(), QStringList() << Collection::mimeType());
+    QCOMPARE(collection.rights(), Collection::CanCreateCollection |
+             Collection::CanChangeCollection |
+             Collection::CanDeleteCollection);
+    const CachePolicy cachePolicy = collection.cachePolicy();
+    QVERIFY(!cachePolicy.inheritFromParent());
+    QCOMPARE(cachePolicy.localParts(), QStringList() << MessagePart::Envelope);
+    QVERIFY(cachePolicy.syncOnDemand());
+}
+
+void TemplateMethodsTest::testMoveCollection()
+{
+    mStore->setPath(mDir.path());
+
+    FileStore::CollectionMoveJob *job = 0;
+
+    // test moving into itself
+    Collection collection(KRandom::random());
+    collection.setParentCollection(mStore->topLevelCollection());
+    collection.setRemoteId(QStringLiteral("collection"));
+    job = mStore->moveCollection(collection, collection);
+    QVERIFY(job != 0);
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+    QVERIFY(!job->errorText().isEmpty());
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(mStore->mErrorCode, job->error());
+    QCOMPARE(mStore->mErrorText, job->errorText());
+
+    QVERIFY(!job->exec());
+
+    // test moving into child
+    Collection childCollection(collection.id() + 1);
+    childCollection.setParentCollection(collection);
+    childCollection.setRemoteId(QStringLiteral("child"));
+    job = mStore->moveCollection(collection, childCollection);
+    QVERIFY(job != 0);
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+    QVERIFY(!job->errorText().isEmpty());
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(mStore->mErrorCode, job->error());
+    QCOMPARE(mStore->mErrorText, job->errorText());
+
+    QVERIFY(!job->exec());
+
+    // test moving into grand child child
+    Collection grandChildCollection(collection.id() + 2);
+    grandChildCollection.setParentCollection(childCollection);
+    grandChildCollection.setRemoteId(QStringLiteral("grandchild"));
+    job = mStore->moveCollection(collection, grandChildCollection);
+    QVERIFY(job != 0);
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+    QVERIFY(!job->errorText().isEmpty());
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(mStore->mErrorCode, job->error());
+    QCOMPARE(mStore->mErrorText, job->errorText());
+
+    QVERIFY(!job->exec());
+
+    // test moving into unrelated collection
+    Collection otherCollection(collection.id() + KRandom::random());
+    otherCollection.setParentCollection(mStore->topLevelCollection());
+    otherCollection.setRemoteId(QStringLiteral("other"));
+    job = mStore->moveCollection(collection, otherCollection);
+    QVERIFY(job != 0);
+    QCOMPARE(job->error(), 0);
+    QVERIFY(job->errorText().isEmpty());
+    QCOMPARE(mStore->mLastCheckedJob, job);
+
+    // collections don't exist in the store, so processing still fails
+    QVERIFY(!job->exec());
+}
+
+void TemplateMethodsTest::testCreateItem()
+{
+    mStore->setPath(mDir.path());
+
+    Collection collection(KRandom::random());
+    collection.setParentCollection(mStore->topLevelCollection());
+    collection.setRemoteId(QStringLiteral("collection"));
+
+    FileStore::ItemCreateJob *job = 0;
+
+    // test item without payload
+    Item item(KMime::Message::mimeType());
+    job = mStore->createItem(item, collection);
+    QVERIFY(job != 0);
+    QCOMPARE(job->error(), (int)FileStore::Job::InvalidJobContext);
+    QVERIFY(!job->errorText().isEmpty());
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(mStore->mErrorCode, job->error());
+    QCOMPARE(mStore->mErrorText, job->errorText());
+
+    QVERIFY(!job->exec());
+
+    // test item with payload
+    item.setPayloadFromData("Subject: dummy payload\n\nwith some content");
+    job = mStore->createItem(item, collection);
+    QVERIFY(job != 0);
+    QCOMPARE(job->error(), 0);
+    QVERIFY(job->errorText().isEmpty());
+    QCOMPARE(mStore->mLastCheckedJob, job);
+
+    // collections don't exist in the store, so processing still fails
+    QVERIFY(!job->exec());
+}
+
+QTEST_MAIN(TemplateMethodsTest)
+
+#include "templatemethodstest.moc"
+
diff --git a/resources/mixedmaildir/autotests/testdata.qrc b/resources/mixedmaildir/autotests/testdata.qrc
new file mode 100644 (file)
index 0000000..1c0332e
--- /dev/null
@@ -0,0 +1,31 @@
+<!DOCTYPE RCC>
+<RCC version="1.0">
+  <qresource>
+    <file>data/mbox</file>
+    <file>data/.mbox.index</file>
+
+    <file>data/mbox-tagged</file>
+    <file>data/.mbox-tagged.index</file>
+
+    <file>data/mbox-unpurged</file>
+    <file>data/.mbox-unpurged.index</file>
+
+    <file>data/maildir/cur/1279979618.4595.CStza_2,S</file>
+    <file>data/maildir/cur/1279979618.4595.pY5ny</file>
+    <file>data/maildir/cur/1279979617.4595.bwXSm</file>
+    <file>data/maildir/cur/1279979618.4595.DUl0I_2,S</file>
+    <file>data/.maildir.index</file>
+
+    <file>data/maildir-tagged/cur/1279982188.18722.6qZsA_2,S</file>
+    <file>data/maildir-tagged/cur/1279982188.18722.Xdz3R_2,S</file>
+    <file>data/maildir-tagged/cur/1279982188.18722.f0l49_2,S</file>
+    <file>data/maildir-tagged/cur/1279982188.18722.kwx1b_2,S</file>
+    <file>data/.maildir-tagged.index</file>
+
+    <file>data/dimap/cur/1279980064.4595.qs6V9_2,S</file>
+    <file>data/dimap/cur/1279980064.4595.LUBVK</file>
+    <file>data/dimap/cur/1279980064.4595.g8PCJ</file>
+    <file>data/dimap/cur/1279980064.4595.RTmAd_2,S</file>
+    <file>data/.dimap.index</file>
+  </qresource>
+</RCC>
diff --git a/resources/mixedmaildir/autotests/testdatatest.cpp b/resources/mixedmaildir/autotests/testdatatest.cpp
new file mode 100644 (file)
index 0000000..f854892
--- /dev/null
@@ -0,0 +1,106 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "testdatautil.h"
+
+#include <QTemporaryDir>
+
+#include <qtest.h>
+#include <QDir>
+
+class TestDataTest : public QObject
+{
+    Q_OBJECT
+public:
+    TestDataTest() {}
+
+private Q_SLOTS:
+    void testResources();
+    void testInstall();
+};
+
+void TestDataTest::testResources()
+{
+    const QStringList testDataNames = TestDataUtil::testDataNames();
+    QCOMPARE(testDataNames, QStringList() << QStringLiteral("dimap")
+             << QStringLiteral("maildir")
+             << QStringLiteral("maildir-tagged")
+             << QStringLiteral("mbox")
+             << QStringLiteral("mbox-tagged")
+             << QStringLiteral("mbox-unpurged"));
+
+    Q_FOREACH (const QString testDataName, testDataNames) {
+        if (testDataName.startsWith(QLatin1String("mbox"))) {
+            QVERIFY(TestDataUtil::folderType(testDataName) == TestDataUtil::MBoxFolder);
+        } else {
+            QVERIFY(TestDataUtil::folderType(testDataName) == TestDataUtil::MaildirFolder);
+        }
+    }
+
+    // TODO check contents?
+}
+
+void TestDataTest::testInstall()
+{
+    QTemporaryDir dir;
+    QDir installDir(dir.path());
+    QDir curDir;
+
+    const QString indexFilePattern = QStringLiteral(".%1.index");
+
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox"), dir.path(), QStringLiteral("mbox1")));
+    QVERIFY(installDir.exists(QLatin1String("mbox1")));
+    QVERIFY(installDir.exists(indexFilePattern.arg(QLatin1String("mbox1"))));
+
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("mbox-tagged"), dir.path(), QStringLiteral("mbox2")));
+    QVERIFY(installDir.exists(QLatin1String("mbox2")));
+    QVERIFY(installDir.exists(indexFilePattern.arg(QLatin1String("mbox2"))));
+
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("maildir"), dir.path(), QStringLiteral("md1")));
+    QVERIFY(installDir.exists(QLatin1String("md1")));
+    QVERIFY(installDir.exists(QLatin1String("md1/new")));
+    QVERIFY(installDir.exists(QLatin1String("md1/cur")));
+    QVERIFY(installDir.exists(QLatin1String("md1/tmp")));
+    QVERIFY(installDir.exists(indexFilePattern.arg(QLatin1String("md1"))));
+
+    curDir = installDir;
+    curDir.cd(QStringLiteral("md1"));
+    curDir.cd(QStringLiteral("cur"));
+    curDir.setFilter(QDir::Files);
+    QCOMPARE((int)curDir.count(), 4);
+
+    QVERIFY(TestDataUtil::installFolder(QLatin1String("maildir-tagged"), dir.path(), QStringLiteral("md2")));
+    QVERIFY(installDir.exists(QLatin1String("md2")));
+    QVERIFY(installDir.exists(QLatin1String("md2/new")));
+    QVERIFY(installDir.exists(QLatin1String("md2/cur")));
+    QVERIFY(installDir.exists(QLatin1String("md2/tmp")));
+    QVERIFY(installDir.exists(indexFilePattern.arg(QLatin1String("md2"))));
+
+    curDir = installDir;
+    curDir.cd(QStringLiteral("md2"));
+    curDir.cd(QStringLiteral("cur"));
+    curDir.setFilter(QDir::Files);
+    QCOMPARE((int)curDir.count(), 4);
+}
+
+#include "testdatatest.moc"
+
+QTEST_MAIN(TestDataTest)
+
diff --git a/resources/mixedmaildir/autotests/testdatautil.cpp b/resources/mixedmaildir/autotests/testdatautil.cpp
new file mode 100644 (file)
index 0000000..dbb5c1f
--- /dev/null
@@ -0,0 +1,201 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "testdatautil.h"
+
+#include "mixedmaildirresource_debug.h"
+
+#include <QDir>
+#include <QFile>
+
+using namespace TestDataUtil;
+
+// use this instead of QFile::copy() because we want overwrite in case it exists
+static bool copyFile(const QString &sourceFileName, const QString &targetFileName)
+{
+    QFile sourceFile(sourceFileName);
+    QFile targetFile(targetFileName);
+
+    if (!sourceFile.open(QIODevice::ReadOnly)) {
+        qCritical() << "Cannot open source file" << sourceFileName;
+        return false;
+    }
+
+    if (!targetFile.open(QIODevice::WriteOnly)) {
+        qCritical() << "Cannot open target file" << targetFileName;
+        return false;
+    }
+
+    return targetFile.write(sourceFile.readAll()) != -1;
+}
+
+static bool copyFiles(const QDir &sourceDir, const QDir &targetDir)
+{
+    const QStringList files = sourceDir.entryList(QStringList(), QDir::Files);
+    Q_FOREACH (const QString &file, files) {
+        const QFileInfo sourceFileInfo(sourceDir, file);
+        const QFileInfo targetFileInfo(targetDir, file);
+        if (!copyFile(sourceFileInfo.absoluteFilePath(), targetFileInfo.absoluteFilePath())) {
+            qCritical() << "Failed to copy" << sourceFileInfo.absoluteFilePath()
+                        << "to" << targetFileInfo.absoluteFilePath();
+            return false;
+        }
+    }
+
+    return true;
+}
+
+FolderType TestDataUtil::folderType(const QString &testDataName)
+{
+    const QDir dir(QStringLiteral(":/data"));
+    const QString indexFilePattern = QStringLiteral(".%1.index");
+
+    if (!dir.exists(testDataName) || !dir.exists(indexFilePattern.arg(testDataName))) {
+        return InvalidFolder;
+    }
+
+    const QFileInfo fileInfo(dir, testDataName);
+    return (fileInfo.isDir() ? MaildirFolder : MBoxFolder);
+}
+
+QStringList TestDataUtil::testDataNames()
+{
+    const QDir dir(QStringLiteral(":/data"));
+    const QFileInfoList dirEntries = dir.entryInfoList();
+
+    const QString indexFilePattern = QStringLiteral(".%1.index");
+
+    QStringList result;
+    Q_FOREACH (const QFileInfo &fileInfo, dirEntries) {
+        if (dir.exists(indexFilePattern.arg(fileInfo.fileName()))) {
+            result << fileInfo.fileName();
+        }
+    }
+
+    result.sort();
+    return result;
+}
+
+bool TestDataUtil::installFolder(const QString &testDataName, const QString &installPath, const QString &folderName)
+{
+    const FolderType type = TestDataUtil::folderType(testDataName);
+    if (type == InvalidFolder) {
+        qCritical() << "testDataName" << testDataName << "is not a valid mail folder type";
+        return false;
+    }
+
+    if (!QDir::current().mkpath(installPath)) {
+        qCritical() << "Couldn't create installPath" << installPath;
+        return false;
+    }
+
+    const QDir installDir(installPath);
+    const QFileInfo installFileInfo(installDir, folderName);
+    if (installDir.exists(folderName)) {
+        switch (type) {
+        case MaildirFolder:
+            if (!installFileInfo.isDir()) {
+                qCritical() << "Target file name" << folderName << "already exists but is not a directory";
+                return false;
+            }
+            break;
+
+        case MBoxFolder:
+            if (!installFileInfo.isFile()) {
+                qCritical() << "Target file name" << folderName << "already exists but is not a directory";
+                return false;
+            }
+            break;
+
+        default:
+            // already handled at beginning
+            Q_ASSERT(false);
+            return false;
+        }
+    }
+
+    const QDir testDataDir(QStringLiteral(":/data"));
+
+    switch (type) {
+    case MaildirFolder: {
+        const QString subPathPattern = QStringLiteral("%1/%2");
+        if (!installDir.mkpath(subPathPattern.arg(folderName, QStringLiteral("new"))) ||
+                !installDir.mkpath(subPathPattern.arg(folderName, QStringLiteral("cur"))) ||
+                !installDir.mkpath(subPathPattern.arg(folderName, QStringLiteral("tmp")))) {
+            qCritical() << "Couldn't create maildir directory structure";
+            return false;
+        }
+
+        QDir sourceDir = testDataDir;
+        QDir targetDir = installDir;
+
+        sourceDir.cd(testDataName);
+        targetDir.cd(folderName);
+
+        if (sourceDir.cd(QStringLiteral("new"))) {
+            targetDir.cd(QStringLiteral("new"));
+            if (!copyFiles(sourceDir, targetDir)) {
+                return false;
+            }
+            sourceDir.cdUp();
+            targetDir.cdUp();
+        }
+
+        if (sourceDir.cd(QStringLiteral("cur"))) {
+            targetDir.cd(QStringLiteral("cur"));
+            if (!copyFiles(sourceDir, targetDir)) {
+                return false;
+            }
+            sourceDir.cdUp();
+            targetDir.cdUp();
+        }
+
+        if (sourceDir.cd(QStringLiteral("tmp"))) {
+            targetDir.cd(QStringLiteral("tmp"));
+            if (!copyFiles(sourceDir, targetDir)) {
+                return false;
+            }
+        }
+        break;
+    }
+
+    case MBoxFolder: {
+        const QFileInfo mboxFileInfo(testDataDir, testDataName);
+        if (!copyFile(mboxFileInfo.absoluteFilePath(), installFileInfo.absoluteFilePath())) {
+            qCritical() << "Failed to copy" << mboxFileInfo.absoluteFilePath()
+                        << "to" << installFileInfo.absoluteFilePath();
+            return false;
+        }
+        break;
+    }
+
+    default:
+        // already handled at beginning
+        Q_ASSERT(false);
+        return false;
+    }
+
+    const QString indexFilePattern = QStringLiteral(".%1.index");
+    const QFileInfo indexFileInfo(testDataDir, indexFilePattern.arg(testDataName));
+    const QFileInfo indexInstallFileInfo(installDir, indexFilePattern.arg(folderName));
+
+    return copyFile(indexFileInfo.absoluteFilePath(), indexInstallFileInfo.absoluteFilePath());
+}
+
diff --git a/resources/mixedmaildir/autotests/testdatautil.h b/resources/mixedmaildir/autotests/testdatautil.h
new file mode 100644 (file)
index 0000000..b3c04a7
--- /dev/null
@@ -0,0 +1,43 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef TESTDATAUTIL_H
+#define TESTDATAUTIL_H
+
+class QString;
+class QStringList;
+
+namespace TestDataUtil
+{
+enum FolderType {
+    InvalidFolder,
+    MaildirFolder,
+    MBoxFolder
+};
+
+FolderType folderType(const QString &testDataName);
+
+QStringList testDataNames();
+
+bool installFolder(const QString &testDataName, const QString &installPath, const QString &folderName);
+}
+
+#endif
+
diff --git a/resources/mixedmaildir/compactchangehelper.cpp b/resources/mixedmaildir/compactchangehelper.cpp
new file mode 100644 (file)
index 0000000..9d2a61e
--- /dev/null
@@ -0,0 +1,236 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "compactchangehelper.h"
+#include "mixedmaildirresource_debug.h"
+#include "filestore/entitycompactchangeattribute.h"
+
+#include <AkonadiCore/collection.h>
+#include <AkonadiCore/collectionmodifyjob.h>
+#include <AkonadiCore/item.h>
+#include <AkonadiCore/itemfetchjob.h>
+#include <AkonadiCore/itemmodifyjob.h>
+#include <AkonadiCore/session.h>
+
+#include <QtCore/QMap>
+#include <QtCore/QQueue>
+#include <QtCore/QVariant>
+
+using namespace Akonadi;
+
+typedef QMap<QString, Item> OldIdItemMap;
+typedef QMap<qint64, OldIdItemMap> RevisionChangeMap;
+typedef QMap<Collection::Id, RevisionChangeMap> CollectionRevisionMap;
+
+struct UpdateBatch {
+    QQueue<Item> items;
+    Collection collection;
+};
+
+class CompactChangeHelper::Private
+{
+    CompactChangeHelper *const q;
+
+public:
+    explicit Private(CompactChangeHelper *parent) : q(parent), mSession(Q_NULLPTR)
+    {
+    }
+
+public:
+    Session *mSession;
+    CollectionRevisionMap mChangesByCollection;
+    QQueue<UpdateBatch> mPendingUpdates;
+    UpdateBatch mCurrentUpdate;
+
+public: // slots
+    void processNextBatch();
+    void processNextItem();
+    void itemFetchResult(KJob *job);
+};
+
+void CompactChangeHelper::Private::processNextBatch()
+{
+    //qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "pendingUpdates.count=" << mPendingUpdates.count();
+    if (mPendingUpdates.isEmpty()) {
+        return;
+    }
+
+    mCurrentUpdate = mPendingUpdates.dequeue();
+
+    processNextItem();
+}
+
+void CompactChangeHelper::Private::processNextItem()
+{
+    //qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "mCurrentUpdate.items.count=" << mCurrentUpdate.items.count();
+    if (mCurrentUpdate.items.isEmpty()) {
+        CollectionModifyJob *job = new CollectionModifyJob(mCurrentUpdate.collection, mSession);
+        QObject::connect(job, SIGNAL(result(KJob*)), q, SLOT(processNextBatch()));
+        return;
+    }
+
+    const Item nextItem = mCurrentUpdate.items.dequeue();
+
+    Item item;
+    item.setRemoteId(nextItem.remoteId());
+
+    ItemFetchJob *job = new ItemFetchJob(item);
+    job->setProperty("oldRemoteId", item.remoteId());
+    job->setProperty("newRemoteId", nextItem.attribute<FileStore::EntityCompactChangeAttribute>()->remoteId());
+    QObject::connect(job, SIGNAL(result(KJob*)), q, SLOT(itemFetchResult(KJob*)));
+}
+
+void CompactChangeHelper::Private::itemFetchResult(KJob *job)
+{
+    ItemFetchJob *fetchJob = qobject_cast<ItemFetchJob *>(job);
+    Q_ASSERT(fetchJob != 0);
+
+    const QString oldRemoteId = fetchJob->property("oldRemoteId").value<QString>();
+    Q_ASSERT(!oldRemoteId.isEmpty());
+
+    const QString newRemoteId = fetchJob->property("newRemoteId").value<QString>();
+    Q_ASSERT(!newRemoteId.isEmpty());
+
+    if (fetchJob->error() != 0) {
+        //qCCritical(MIXEDMAILDIRRESOURCE_LOG) << "Item fetch for remoteId=" << oldRemoteId
+        //         << "new remoteId=" << newRemoteId << "failed:" << fetchJob->errorString();
+        processNextItem();
+        return;
+    }
+
+    // since we only need the item to modify its remote ID, we don't care
+    // if it does not exist (anymore)
+    if (fetchJob->items().isEmpty()) {
+        processNextItem();
+        return;
+    }
+
+    const Item item = fetchJob->items().at(0);
+
+    Item updatedItem(item);
+    updatedItem.setRemoteId(newRemoteId);
+
+    ItemModifyJob *modifyJob = new ItemModifyJob(updatedItem);
+    QObject::connect(modifyJob, SIGNAL(result(KJob*)), q, SLOT(processNextItem()));
+}
+
+CompactChangeHelper::CompactChangeHelper(const QByteArray &sessionId, QObject *parent)
+    : QObject(parent), d(new Private(this))
+{
+    d->mSession = new Session(sessionId, this);
+}
+
+CompactChangeHelper::~CompactChangeHelper()
+{
+    delete d;
+}
+
+void CompactChangeHelper::addChangedItems(const Item::List &items)
+{
+    if (items.isEmpty()) {
+        return;
+    }
+
+    qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "items.count=" << items.count()
+                                      << "pendingUpdates.count=" << d->mPendingUpdates.count();
+    UpdateBatch updateBatch;
+
+    Q_FOREACH (const Item &item, items) {
+        const Collection collection = item.parentCollection();
+        const qint64 revision = collection.remoteRevision().toLongLong();
+
+        RevisionChangeMap &changesByRevision = d->mChangesByCollection[ collection.id() ];
+        OldIdItemMap &changes = changesByRevision[ revision ];
+        changes.insert(item.remoteId(), item);
+
+        if (!updateBatch.collection.isValid()) {
+            updateBatch.collection = collection;
+        } else if (updateBatch.collection != collection) {
+            d->mPendingUpdates << updateBatch;
+            updateBatch.items.clear();
+            updateBatch.collection = collection;
+        }
+
+        updateBatch.items << item;
+    }
+
+    if (updateBatch.collection.isValid()) {
+        d->mPendingUpdates << updateBatch;
+    }
+
+    QMetaObject::invokeMethod(this, "processNextBatch", Qt::QueuedConnection);
+}
+
+QString CompactChangeHelper::currentRemoteId(const Item &item) const
+{
+    const Collection collection = item.parentCollection();
+    const qint64 revision = collection.remoteRevision().toLongLong();
+
+    QString remoteId = item.remoteId();
+
+    const CollectionRevisionMap::const_iterator colIt = d->mChangesByCollection.constFind(collection.id());
+    if (colIt != d->mChangesByCollection.constEnd()) {
+        // find revision and iterate until the highest available one
+        RevisionChangeMap::const_iterator revIt = colIt->constFind(revision);
+        for (; revIt != colIt->constEnd(); ++revIt) {
+            const OldIdItemMap::const_iterator idIt = revIt->constFind(remoteId);
+            if (idIt != revIt->constEnd()) {
+                remoteId = idIt.value().attribute<FileStore::EntityCompactChangeAttribute>()->remoteId();
+            } else {
+                break;
+            }
+        }
+    }
+
+    if (item.remoteId() != remoteId) {
+        qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "item (id=" << item.id() << "remoteId=" << item.remoteId()
+                                          << "), col(id=" << collection.id() << ", name=" << collection.name()
+                                          << ", revision=" << revision << ") in compact change set (revisions="
+                                          << colIt->keys() << ": current remoteId=" << remoteId;
+    }
+
+    return remoteId;
+}
+
+void CompactChangeHelper::checkCollectionChanged(const Collection &collection)
+{
+    const qint64 revision = collection.remoteRevision().toLongLong();
+    //qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "col.id=" << collection.id() << ", remoteId=" << collection.remoteId()
+    //         << "revision=" << revision;
+
+    const CollectionRevisionMap::iterator colIt = d->mChangesByCollection.find(collection.id());
+    if (colIt != d->mChangesByCollection.end()) {
+        qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "matching change map found with" << colIt->count() << "entries";
+        // remove all revisions until the seen one appears
+        RevisionChangeMap::iterator revIt = colIt->begin();
+        while (revIt != colIt->end() && revIt.key() <= revision) {
+            qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "removing entry for revision" << revIt.key();
+            revIt = colIt->erase(revIt);
+        }
+
+        if (revIt == colIt->end()) {
+            qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "all change maps gone";
+            d->mChangesByCollection.erase(colIt);
+        }
+    }
+}
+
+#include "moc_compactchangehelper.cpp"
+
diff --git a/resources/mixedmaildir/compactchangehelper.h b/resources/mixedmaildir/compactchangehelper.h
new file mode 100644 (file)
index 0000000..04d7a56
--- /dev/null
@@ -0,0 +1,61 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef COMPACTCHANGEHELPER_H
+#define COMPACTCHANGEHELPER_H
+
+#include <QObject>
+
+template <typename T> class QList;
+
+namespace Akonadi
+{
+class Collection;
+class Item;
+
+typedef QVector<Item> ItemList;
+}
+
+class CompactChangeHelper : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit CompactChangeHelper(const QByteArray &sessionId, QObject *parent = Q_NULLPTR);
+
+    ~CompactChangeHelper();
+
+    void addChangedItems(const Akonadi::ItemList &items);
+
+    QString currentRemoteId(const Akonadi::Item &item) const;
+
+    void checkCollectionChanged(const Akonadi::Collection &collection);
+
+private:
+    class Private;
+    Private *const d;
+
+    Q_PRIVATE_SLOT(d, void processNextBatch())
+    Q_PRIVATE_SLOT(d, void processNextItem())
+    Q_PRIVATE_SLOT(d, void itemFetchResult(KJob *))
+};
+
+#endif
+
diff --git a/resources/mixedmaildir/configdialog.cpp b/resources/mixedmaildir/configdialog.cpp
new file mode 100644 (file)
index 0000000..b4f3dae
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+    Copyright (c) 2008 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "configdialog.h"
+#include "settings.h"
+
+#include "libmaildir/maildir.h"
+
+#include <kconfigdialogmanager.h>
+#include <kurlrequester.h>
+#include <klineedit.h>
+#include <QVBoxLayout>
+#include <QDialogButtonBox>
+using KPIM::Maildir;
+
+ConfigDialog::ConfigDialog(QWidget *parent) :
+    QDialog(parent),
+    mToplevelIsContainer(false)
+{
+    setWindowTitle(i18n("Select a KMail Mail folder"));
+    QWidget *mainWidget = new QWidget(this);
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    setLayout(mainLayout);
+    mainLayout->addWidget(mainWidget);
+    ui.setupUi(mainWidget);
+    mManager = new KConfigDialogManager(this, Settings::self());
+    mManager->updateWidgets();
+    ui.kcfg_Path->setMode(KFile::Directory | KFile::ExistingOnly);
+    ui.kcfg_Path->setUrl(QUrl::fromLocalFile(Settings::self()->path()));
+
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    mainLayout->addWidget(buttonBox);
+    mOkButton = buttonBox->button(QDialogButtonBox::Ok);
+    mOkButton->setDefault(true);
+    mOkButton->setShortcut(Qt::CTRL | Qt::Key_Return);
+    connect(buttonBox, &QDialogButtonBox::accepted, this, &ConfigDialog::accept);
+    connect(buttonBox, &QDialogButtonBox::rejected, this, &ConfigDialog::reject);
+
+    connect(mOkButton, &QPushButton::clicked, this, &ConfigDialog::save);
+    connect(ui.kcfg_Path->lineEdit(), &QLineEdit::textChanged, this, &ConfigDialog::checkPath);
+    ui.kcfg_Path->lineEdit()->setFocus();
+    checkPath();
+}
+
+void ConfigDialog::checkPath()
+{
+    if (ui.kcfg_Path->url().isEmpty()) {
+        ui.statusLabel->setText(i18n("The selected path is empty."));
+        mOkButton->setEnabled(false);
+        return;
+    }
+    bool ok = false;
+    mToplevelIsContainer = false;
+    QDir d(ui.kcfg_Path->url().toLocalFile());
+
+    if (d.exists()) {
+        Maildir md(d.path());
+        if (!md.isValid()) {
+            Maildir md2(d.path(), true);
+            if (md2.isValid()) {
+                ui.statusLabel->setText(i18n("The selected path contains valid Maildir folders."));
+                mToplevelIsContainer = true;
+                ok = true;
+            } else {
+                ui.statusLabel->setText(md.lastError());
+            }
+        } else {
+            ui.statusLabel->setText(i18n("The selected path is a valid Maildir."));
+            ok = true;
+        }
+    } else {
+        d.cdUp();
+        if (d.exists()) {
+            ui.statusLabel->setText(i18n("The selected path does not exist yet, a new Maildir will be created."));
+            ok = true;
+        } else {
+            ui.statusLabel->setText(i18n("The selected path does not exist."));
+        }
+    }
+    mOkButton->setEnabled(ok);
+}
+
+void ConfigDialog::save()
+{
+    mManager->updateSettings();
+    Settings::self()->setPath(ui.kcfg_Path->url().isLocalFile() ? ui.kcfg_Path->url().toLocalFile()  : ui.kcfg_Path->url().path());
+    Settings::self()->setTopLevelIsContainer(mToplevelIsContainer);
+    Settings::self()->save();
+}
+
diff --git a/resources/mixedmaildir/configdialog.h b/resources/mixedmaildir/configdialog.h
new file mode 100644 (file)
index 0000000..bef9159
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+    Copyright (c) 2008 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef CONFIGDIALOG_H
+#define CONFIGDIALOG_H
+
+#include <QDialog>
+
+#include "ui_settings.h"
+class QPushButton;
+
+class KConfigDialogManager;
+
+class ConfigDialog : public QDialog
+{
+    Q_OBJECT
+public:
+    explicit ConfigDialog(QWidget *parent = Q_NULLPTR);
+
+private Q_SLOTS:
+    void checkPath();
+    void save();
+
+private:
+    Ui::ConfigDialog ui;
+    KConfigDialogManager *mManager;
+    QPushButton *mOkButton;
+    bool mToplevelIsContainer;
+};
+
+#endif
diff --git a/resources/mixedmaildir/kmindexreader/CMakeLists.txt b/resources/mixedmaildir/kmindexreader/CMakeLists.txt
new file mode 100644 (file)
index 0000000..7467d45
--- /dev/null
@@ -0,0 +1,23 @@
+if (BUILD_TESTING)
+   add_subdirectory(autotests)
+endif()
+
+########### next target ###############
+
+set(kmindexreader_LIB_SRCS
+ kmindexreader.cpp
+ ../mixedmaildir_debug.cpp
+ ../mixedmaildirresource_debug.cpp
+)
+
+add_library(kmindexreader  ${kmindexreader_LIB_SRCS})
+generate_export_header(kmindexreader BASE_NAME kmindexreader)
+
+target_link_libraries(kmindexreader
+    KF5::AkonadiMime
+    KF5::KDELibs4Support
+)
+
+set_target_properties(kmindexreader PROPERTIES VERSION ${KDEPIMRUNTIME_LIB_VERSION} SOVERSION ${KDEPIMRUNTIME_LIB_SOVERSION} )
+
+install(TARGETS kmindexreader ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP)
diff --git a/resources/mixedmaildir/kmindexreader/autotests/CMakeLists.txt b/resources/mixedmaildir/kmindexreader/autotests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..db45dda
--- /dev/null
@@ -0,0 +1,15 @@
+add_definitions(-DTEST_PATH=${CMAKE_CURRENT_SOURCE_DIR})
+include_directories(
+    ${CMAKE_CURRENT_SOURCE_DIR}
+    ${mixedmaildirresource_SOURCE_DIR}/kmindexreader
+)
+
+set( testidxreader_SRCS
+    testidxreader.cpp
+)
+remove_definitions(-DQT_NO_CAST_FROM_ASCII)
+add_executable( testidxreader  ${testidxreader_SRCS} )
+add_test( testidxreader testidxreader )
+ecm_mark_as_test(testidxreader)
+
+target_link_libraries( testidxreader  Qt5::Test kmindexreader KF5::AkonadiMime )
diff --git a/resources/mixedmaildir/kmindexreader/autotests/TestIdxReader_data.h b/resources/mixedmaildir/kmindexreader/autotests/TestIdxReader_data.h
new file mode 100644 (file)
index 0000000..59f823a
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ *   Copyright (C) 2010 Casey Link <unnamedrambler@gmail.com>
+ *   Copyright (c) 2009-2010 Klaralvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License along
+ *   with this program; if not, write to the Free Software Foundation, Inc.,
+ *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ */
+
+#ifndef TESTIDXREADER_DATA
+#define TESTIDXREADER_DATA
+
+/* contains one maildir with one  email inside*/
+static const char mailDirOneEmail[] =
+    "IyBLTWFpbC1JbmRleCBWMTUwNgoACAAAAHhWNBIIAAAAagEAAAUAAAAAAAgAAAAIAAAAAAAAAAAA"
+    "AQAAABQAAEMAYQBzAGUAeQAgAEwAaQBuAGsTAAAAAAACAAAAIAAAaABlAGwAbABvACAAZgByAG8A"
+    "bQAgAGsAbQBhAGkAbAMAAAAcAABLAEQAQQBCACAARQBtAHAAbABvAHkAZQBlAHMEAAAAAAAGAAAA"
+    "AAALAAAANAAAMQAyADcAMQA1ADEAMwA1ADMANwAuADEAOQA1ADcANgAuADEATABLADYAZQA6ADIA"
+    "LABTCQAAAAgAhAIAAAAAAAAHAAAACAAAAAAAAAAAAAoAAAAIAMHByUsAAAAADAAAAAgATgBOAAAA"
+    "AAANAAAACAAgAAAAAAAAAA4AAAAAAA8AAAAsAABxADEAUABJAFkAbgBhAGwAVwB3AFYAQgAzAHEA"
+    "aAB2AEYAUABUAHQAaQBBEAAAAAgABAAAAAAAAAARAAAACAAAAAAAAAAAABIAAAAIAAAAAAAAAAAA";
+
+/*
+ Folder: "Test1" type:Maildir
+ Message1: "foo bar"  Important, Unread
+ Message2: "hello from kmail" Read, tagged "N5tUHPOZFf"
+ */
+static const char mailDirTwoEmailOneTagFlags[] =
+    "IyBLTWFpbC1JbmRleCBWMTUwNgoACAAAAHhWNBIIAAAAlAEAAAUAAAAAAAgAAAAIAAAAAAAAAAAA"
+    "AQAAABQAAEMAYQBzAGUAeQAgAEwAaQBuAGsTAAAAKgAATgA1AHQAVQBIAFAATwBaAEYAZgAsAFUA"
+    "RgBNAEUAagBHAHMAUQA5AG8CAAAAIAAAaABlAGwAbABvACAAZgByAG8AbQAgAGsAbQBhAGkAbAMA"
+    "AAAcAABLAEQAQQBCACAARQBtAHAAbABvAHkAZQBlAHMEAAAAAAAGAAAAAAALAAAANAAAMQAyADcA"
+    "MQA1ADEAMwA1ADMANwAuADEAOQA1ADcANgAuADEATABLADYAZQA6ADIALABTCQAAAAgAhAIAAAAA"
+    "AAAHAAAACAAAAAAAAAAAAAoAAAAIAMHByUsAAAAADAAAAAgATgBOAAAAAAANAAAACAAgAAAAAAAA"
+    "AA4AAAAAAA8AAAAsAABxADEAUABJAFkAbgBhAGwAVwB3AFYAQgAzAHEAaAB2AEYAUABUAHQAaQBB"
+    "EAAAAAgABAAAAAAAAAARAAAACAAAAAAAAAAAABIAAAAIAAAAAAAAAAAASAEAAAUAAAAAAAgAAAAI"
+    "AAAAAAAAAAAAAQAAABQAAEMAYQBzAGUAeQAgAEwAaQBuAGsTAAAAAAACAAAADgAAZgBvAG8AIABi"
+    "AGEAcgMAAAAWAABvAG4AZQBAAG8AbgBlAC4AYwBvAG0EAAAAAAAGAAAAAAALAAAAKgAAMQAyADcA"
+    "MQA3ADcAMwAwADEAOQAuADUANwA1ADQALgBwAFcAcQBaAFMJAAAACABKAgAAAAAAAAcAAAAIAAAA"
+    "AAAAAAAACgAAAAgAW7fNSwAAAAAMAAAACABOAE4AAAAAAA0AAAAIACAAAAAAAAAADgAAAAAADwAA"
+    "ACwAAE0AbgB0AHYAQgAwAE4AWQBFAFMATwBiAHgASAA0AFYAUgBEAFUAeQBjAHcQAAAACAACAgAA"
+    "AAAAABEAAAAIAAAAAAAAAAAAEgAAAAgAAAAAAAAAAAA=";
+
+#endif
diff --git a/resources/mixedmaildir/kmindexreader/autotests/data/.keep b/resources/mixedmaildir/kmindexreader/autotests/data/.keep
new file mode 100644 (file)
index 0000000..2f1d07a
--- /dev/null
@@ -0,0 +1 @@
+force git to include this file
diff --git a/resources/mixedmaildir/kmindexreader/autotests/testidxreader.cpp b/resources/mixedmaildir/kmindexreader/autotests/testidxreader.cpp
new file mode 100644 (file)
index 0000000..2a94874
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ *   Copyright (C) 2010 Casey Link <unnamedrambler@gmail.com>
+ *   Copyright (c) 2009-2010 Klaralvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License along
+ *   with this program; if not, write to the Free Software Foundation, Inc.,
+ *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ */
+
+#include "testidxreader.h"
+
+#include "../kmindexreader.h"
+
+#include "TestIdxReader_data.h"
+
+#include <akonadi/kmime/messagestatus.h>
+using Akonadi::MessageStatus;
+#include <QTemporaryFile>
+
+#include <qtest.h>
+#include <QtTest/QTest>
+#include <QDebug>
+
+QTEST_MAIN(TestIdxReader)
+
+TestIdxReader::TestIdxReader()
+{
+}
+
+void TestIdxReader::testError()
+{
+    KMIndexReader reader(QStringLiteral("IDoNotExist"));
+
+    QVERIFY(reader.error() == true);
+}
+
+void TestIdxReader::testReadHeader()
+{
+    QTemporaryFile tmp;
+    if (!tmp.open()) {
+        qDebug() << "Could not open temp file.";
+        return;
+    }
+    tmp.write(QByteArray::fromBase64(mailDirOneEmail));
+    tmp.close();
+    KMIndexReader reader(tmp.fileName());
+
+    QVERIFY(reader.error() == false);
+
+    int version = 0;
+    bool success = reader.readHeader(&version);
+
+    QVERIFY(success == true);
+    QCOMPARE(version, 1506);
+
+    QVERIFY(reader.error() == false);
+}
+
+void TestIdxReader::testRead()
+{
+    QTemporaryFile tmp;
+    if (!tmp.open()) {
+        qDebug() << "Could not open temp file.";
+        return;
+    }
+    tmp.write(QByteArray::fromBase64(mailDirTwoEmailOneTagFlags));
+    tmp.close();
+    KMIndexReader reader(tmp.fileName());
+    QVERIFY(reader.error() == false);
+    bool success = reader.readIndex();
+    QVERIFY(success == true);
+
+    QVERIFY(reader.messages().size() == 2);
+
+    KMIndexDataPtr msg = reader.messages().front();
+
+    QString subject = msg->mCachedStringParts[KMIndexReader::MsgSubjectPart];
+    MessageStatus status;
+    status.fromQInt32(msg->mCachedLongParts[KMIndexReader::MsgStatusPart]);
+    QCOMPARE(subject, QString("hello from kmail"));
+    QVERIFY(!status.isImportant());
+    QVERIFY(!msg->status().isImportant());
+    QVERIFY(msg->status().isRead());
+    QVERIFY(msg->tagList().contains("N5tUHPOZFf"));
+
+    msg = reader.messages().back();
+    status.fromQInt32(msg->mCachedLongParts[KMIndexReader::MsgStatusPart]);
+    subject = msg->mCachedStringParts[KMIndexReader::MsgSubjectPart];
+    QCOMPARE(subject, QString("foo bar"));
+    QVERIFY(status.isImportant());
+    QVERIFY(msg->status().isImportant());
+    QVERIFY(!msg->status().isRead());
+    QVERIFY(msg->tagList().size() == 0);
+}
+
diff --git a/resources/mixedmaildir/kmindexreader/autotests/testidxreader.h b/resources/mixedmaildir/kmindexreader/autotests/testidxreader.h
new file mode 100644 (file)
index 0000000..08f870a
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ *   Copyright (C) 2010 Casey Link <unnamedrambler@gmail.com>
+ *   Copyright (c) 2009-2010 Klaralvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License along
+ *   with this program; if not, write to the Free Software Foundation, Inc.,
+ *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ */
+
+#ifndef TESTIDXREADER_H
+#define TESTIDXREADER_H
+
+#include <QtCore/QObject>
+
+class TestIdxReader : public QObject
+{
+    Q_OBJECT
+public:
+    TestIdxReader();
+private Q_SLOTS:
+    void testError();
+    void testReadHeader();
+    void testRead();
+private:
+
+};
+
+#endif // TESTIDXREADER_H
diff --git a/resources/mixedmaildir/kmindexreader/kmindexreader.cpp b/resources/mixedmaildir/kmindexreader/kmindexreader.cpp
new file mode 100644 (file)
index 0000000..c449cdf
--- /dev/null
@@ -0,0 +1,617 @@
+/*
+ *   Copyright (C) 2010 Casey Link <unnamedrambler@gmail.com>
+ *   Copyright (c) 2009-2010 Klaralvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
+
+ *   This file includes code from old files from previous KDE versions:
+ *   Copyright (c) 2003 Andreas Gungl <a.gungl@gmx.de>
+ *   Copyright (c) 1996-1998 Stefan Taferner <taferner@kde.org>
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License along
+ *   with this program; if not, write to the Free Software Foundation, Inc.,
+ *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ */
+
+#include "kmindexreader.h"
+
+#include "../mixedmaildirresource_debug.h"
+#include "../mixedmaildir_debug.h"
+#include <kde_file.h>
+#include <akonadi/kmime/messagestatus.h>
+using Akonadi::MessageStatus;
+#include <QFile>
+
+//BEGIN: Magic definitions from old kmail code
+#ifdef HAVE_BYTESWAP_H
+#include <byteswap.h>
+#endif
+
+static const int INDEX_VERSION = 1506;
+const size_t readCount = 1;
+#ifndef MAX_LINE
+static const int MAX_LINE = 4096;
+#endif
+
+// We define functions as kmail_swap_NN so that we don't get compile errors
+// on platforms where bswap_NN happens to be a function instead of a define.
+
+/* Swap bytes in 16 bit value.  */
+#ifdef bswap_16
+#define kmail_swap_16(x) bswap_16(x)
+#else
+#define kmail_swap_16(x) \
+    ((((x) >> 8) & 0xff) | (((x) & 0xff) << 8))
+#endif
+
+/* Swap bytes in 32 bit value.  */
+#ifdef bswap_32
+#define kmail_swap_32(x) bswap_32(x)
+#else
+#define kmail_swap_32(x) \
+    ((((x) & 0xff000000) >> 24) | (((x) & 0x00ff0000) >>  8) |                      \
+     (((x) & 0x0000ff00) <<  8) | (((x) & 0x000000ff) << 24))
+#endif
+
+/* Swap bytes in 64 bit value.  */
+#ifdef bswap_64
+#define kmail_swap_64(x) bswap_64(x)
+#else
+#define kmail_swap_64(x) \
+    ((((x) & 0xff00000000000000ull) >> 56)                      \
+     | (((x) & 0x00ff000000000000ull) >> 40)                                      \
+     | (((x) & 0x0000ff0000000000ull) >> 24)                                      \
+     | (((x) & 0x000000ff00000000ull) >> 8)                                      \
+     | (((x) & 0x00000000ff000000ull) << 8)                                      \
+     | (((x) & 0x0000000000ff0000ull) << 24)                                      \
+     | (((x) & 0x000000000000ff00ull) << 40)                                      \
+     | (((x) & 0x00000000000000ffull) << 56))
+#endif
+
+/** The old status format, only one at a time possible. Needed
+    for upgrade path purposes. */
+typedef enum {
+    KMLegacyMsgStatusUnknown = ' ',
+    KMLegacyMsgStatusNew = 'N',
+    KMLegacyMsgStatusUnread = 'U',
+    KMLegacyMsgStatusRead = 'R',
+    KMLegacyMsgStatusOld = 'O',
+    KMLegacyMsgStatusDeleted = 'D',
+    KMLegacyMsgStatusReplied = 'A',
+    KMLegacyMsgStatusForwarded = 'F',
+    KMLegacyMsgStatusQueued = 'Q',
+    KMLegacyMsgStatusSent = 'S',
+    KMLegacyMsgStatusFlag = 'G'
+} KMLegacyMsgStatus;
+
+//END: Magic definitions from old kmail code
+
+//BEGIN: KMIndexMsg methods
+
+KMIndexData::KMIndexData() : mPartsCacheBuilt(false)
+{
+    const uint count = sizeof(mCachedLongParts) / sizeof(unsigned long);
+    for (uint i = 0; i < count; ++i) {
+        mCachedLongParts[ i ] = 0;
+    }
+}
+
+MessageStatus &KMIndexData::status()
+{
+    if (mStatus.isOfUnknownStatus()) {
+        mStatus.fromQInt32(mCachedLongParts[KMIndexReader::MsgStatusPart]);
+        if (mStatus.isOfUnknownStatus()) {
+            // We are opening an old index for the first time, get the legacy
+            // status and merge it in.
+            // This is kept to provide an upgrade path from the old single
+            // status to the new multiple status scheme.
+            KMLegacyMsgStatus legacyMsgStatus = (KMLegacyMsgStatus) mCachedLongParts[KMIndexReader::MsgLegacyStatusPart];
+            mStatus.setRead();
+            switch (legacyMsgStatus) {
+            case KMLegacyMsgStatusUnknown:
+                mStatus.clear();
+                break;
+            case KMLegacyMsgStatusUnread:
+                mStatus.setRead(false);
+                break;
+            case KMLegacyMsgStatusRead:
+                mStatus.setRead();
+                break;
+            case KMLegacyMsgStatusDeleted:
+                mStatus.setDeleted();
+                break;
+            case KMLegacyMsgStatusReplied:
+                mStatus.setReplied();
+                break;
+            case KMLegacyMsgStatusForwarded:
+                mStatus.setForwarded();
+                break;
+            case KMLegacyMsgStatusQueued:
+                mStatus.setQueued();
+                break;
+            case KMLegacyMsgStatusSent:
+                mStatus.setSent();
+                break;
+            case KMLegacyMsgStatusFlag:
+                mStatus.setImportant();
+                break;
+            default:
+                break;
+            }
+
+        }
+    }
+    return mStatus;
+}
+
+QStringList KMIndexData::tagList() const
+{
+    return mCachedStringParts[KMIndexReader::MsgTagPart].split(QLatin1Char(','), QString::SkipEmptyParts);
+}
+
+quint64 KMIndexData::uid() const
+{
+    return mCachedLongParts[KMIndexReader::MsgUIDPart];
+}
+
+bool KMIndexData::isEmpty() const
+{
+    return !mPartsCacheBuilt;
+}
+
+//END: KMIndexMsg methods
+
+KMIndexReader::KMIndexReader(const QString &indexFile)
+    : mIndexFileName(indexFile)
+    , mIndexFile(indexFile)
+    , mConvertToUtf8(false)
+    , mIndexSwapByteOrder(false)
+    , mHeaderOffset(0)
+    , mError(false)
+{
+    if (!mIndexFile.exists()) {
+        qCDebug(MIXEDMAILDIR_LOG) << "file doesn't exist";
+        mError = true;
+    }
+
+    if (!mIndexFile.open(QIODevice::ReadOnly)) {
+        qCDebug(MIXEDMAILDIR_LOG) << "file cant be open";
+        mError = true;
+    }
+
+    mFp = fdopen(mIndexFile.handle(), "r");
+}
+
+KMIndexReader::~KMIndexReader()
+{
+    if (mFp) {
+        fclose(mFp);
+    }
+}
+
+bool KMIndexReader::error() const
+{
+    return mError;
+}
+
+KMIndexDataPtr KMIndexReader::dataByOffset(quint64 offset) const
+{
+    QHash<quint64, KMIndexDataPtr>::const_iterator it = mMsgByOffset.constFind(offset);
+    if (it == mMsgByOffset.constEnd()) {
+        return KMIndexDataPtr();
+    }
+
+    return it.value();
+}
+
+KMIndexDataPtr KMIndexReader::dataByFileName(const QString &fileName) const
+{
+    QHash<QString, KMIndexDataPtr>::const_iterator it = mMsgByFileName.constFind(fileName);
+    if (it == mMsgByFileName.constEnd()) {
+        return KMIndexDataPtr();
+    }
+
+    return it.value();
+}
+
+bool KMIndexReader::readHeader(int *version)
+{
+    int indexVersion;
+    Q_ASSERT(mFp != 0);
+    mIndexSwapByteOrder = false;
+    mIndexSizeOfLong = sizeof(long);
+
+    int ret = fscanf(mFp, "# KMail-Index V%d\n", &indexVersion);
+    if (ret == EOF || ret == 0) {
+        return false;    // index file has invalid header
+    }
+    if (version) {
+        *version = indexVersion;
+    }
+    if (indexVersion < 1505) {
+        if (indexVersion == 1503) {
+            qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "Need to convert old index file" << mIndexFileName << "to utf-8";
+            mConvertToUtf8 = true;
+        }
+        return true;
+    } else if (indexVersion == 1505) {
+    } else if (indexVersion < INDEX_VERSION) {
+        qCCritical(MIXEDMAILDIR_LOG) << "Index file" << mIndexFileName << "is out of date. What to do?";
+//       createIndexFromContents();
+        return false;
+    } else if (indexVersion > INDEX_VERSION) {
+        qFatal("index file of newer version");
+        return false;
+    } else {
+        // Header
+        quint32 byteOrder = 0;
+        quint32 sizeOfLong = sizeof(long);   // default
+
+        quint32 header_length = 0;
+        KDE_fseek(mFp, sizeof(char), SEEK_CUR);
+        if (fread(&header_length, sizeof(header_length), readCount, mFp) != readCount) {
+            qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "Failed to read header_length";
+            return false;
+        }
+        if (header_length > 0xFFFF) {
+            header_length = kmail_swap_32(header_length);
+        }
+
+        off_t endOfHeader = KDE_ftell(mFp) + header_length;
+
+        bool needs_update = true;
+        // Process available header parts
+        if (header_length >= sizeof(byteOrder)) {
+            if (fread(&byteOrder, sizeof(byteOrder), readCount, mFp) != readCount) {
+                qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "Failed to read byteOrder";
+                return false;
+            }
+            mIndexSwapByteOrder = (byteOrder == 0x78563412);
+            header_length -= sizeof(byteOrder);
+
+            if (header_length >= sizeof(sizeOfLong)) {
+                if (fread(&sizeOfLong, sizeof(sizeOfLong), readCount, mFp) != readCount) {
+                    qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "Failed to read sizeOfLong";
+                    return false;
+                }
+                if (mIndexSwapByteOrder) {
+                    sizeOfLong = kmail_swap_32(sizeOfLong);
+                }
+                mIndexSizeOfLong = sizeOfLong;
+                header_length -= sizeof(sizeOfLong);
+                needs_update = false;
+            }
+        }
+        if (needs_update || mIndexSwapByteOrder || (mIndexSizeOfLong != sizeof(long))) {
+            qCDebug(MIXEDMAILDIR_LOG) << "DIRTY!";
+//         setDirty( true );
+        }
+        // Seek to end of header
+        KDE_fseek(mFp, endOfHeader, SEEK_SET);
+
+        if (mIndexSwapByteOrder) {
+            qCDebug(MIXEDMAILDIR_LOG) << "Index File has byte order swapped!";
+        }
+        if (mIndexSizeOfLong != sizeof(long)) {
+            qCDebug(MIXEDMAILDIR_LOG) << "Index File sizeOfLong is" << mIndexSizeOfLong << "while sizeof(long) is" << sizeof(long) << "!";
+        }
+
+    }
+    return true;
+}
+
+bool KMIndexReader::readIndex()
+{
+    qint32 len;
+    KMIndexData *msg;
+
+    Q_ASSERT(mFp != 0);
+    rewind(mFp);
+
+    mMsgList.clear();
+    mMsgByFileName.clear();
+    mMsgByOffset.clear();
+
+    int version;
+
+    if (!readHeader(&version)) {
+        return false;
+    }
+
+    mHeaderOffset = KDE_ftell(mFp);
+
+    // loop through the entire index
+    while (!feof(mFp)) {
+        //qCDebug(MIXEDMAILDIR_LOG) << "NEW MSG!";
+        msg = Q_NULLPTR;
+        // check version (parsed by readHeader)
+        // because different versions must be
+        // parsed differently
+        //qCDebug(MIXEDMAILDIR_LOG) << "parsing version" << version;
+        if (version >= 1505) {
+            // parse versions >= 1505
+            if (!fread(&len, sizeof(len), 1, mFp)) {
+                break;
+            }
+
+            // swap bytes if needed
+            if (mIndexSwapByteOrder) {
+                len = kmail_swap_32(len);
+            }
+
+            off_t offs = KDE_ftell(mFp);
+            if (KDE_fseek(mFp, len, SEEK_CUR)) {
+                break;
+            }
+            msg = new KMIndexData();
+            fillPartsCache(msg, offs, len);
+        } else {
+            //////////////////////
+            //BEGIN UNTESTED CODE
+            //////////////////////
+            //parse verions < 1505
+            QByteArray line(MAX_LINE, '\0');
+            if (fgets(line.data(), MAX_LINE, mFp) == Q_NULLPTR) {
+                break;
+            }
+            if (feof(mFp)) {
+                break;
+            }
+            if (*line.data() == '\0') {
+                // really, i have no idea when or how this would occur
+                // but we probably want to know if it does - Casey
+                qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "Unknowable bad occurred";
+                qCDebug(MIXEDMAILDIR_LOG) << "fclose(mFp = " << mFp << ")";
+                fclose(mFp);
+                mFp = Q_NULLPTR;
+                mMsgList.clear();
+                mMsgByFileName.clear();
+                mMsgByOffset.clear();
+                return false;
+            }
+            off_t offs = KDE_ftell(mFp);
+            if (KDE_fseek(mFp, len, SEEK_CUR)) {
+                break;
+            }
+            msg = new KMIndexData;
+            fromOldIndexString(msg, line, mConvertToUtf8);
+
+            fillPartsCache(msg, offs, len);
+            //////////////////////
+            //END UNTESTED CODE
+            //////////////////////
+        }
+        if (!msg) {
+            break;
+        }
+
+        if (msg->status().isDeleted()) {
+            delete msg;  // skip messages that are marked as deleted
+            continue;
+        }
+#ifdef OBSOLETE
+//     else if (mi->isNew())
+//     {
+//       mi->setStatus(KMMsgStatusUnread);
+//       mi->setDirty(false);
+//     }
+#endif
+        KMIndexDataPtr msgPtr(msg);
+        mMsgList.append(msgPtr);
+        const QString fileName = msg->mCachedStringParts[ MsgFilePart ];
+        if (!fileName.isEmpty()) {
+            mMsgByFileName.insert(fileName, msgPtr);
+        }
+
+        const quint64 offset = msg->mCachedLongParts[ MsgOffsetPart ];
+        if (offset > 0) {
+            mMsgByOffset.insert(offset, msgPtr);
+        }
+    } // end while
+
+    return true;
+}
+
+//--- For compatibility with old index files
+bool KMIndexReader::fromOldIndexString(KMIndexData *msg, const QByteArray &str, bool toUtf8)
+{
+    Q_UNUSED(toUtf8)
+//     const char *start, *offset;
+//   msg->modifiers = KMMsgInfoPrivate::ALL_SET;
+//   msg->xmark   = str.mid(33, 3).trimmed();
+//   msg->folderOffset = str.mid(2,9).toULong();
+//   msg->msgSize = str.mid(12,9).toULong();
+//   msg->date = (time_t)str.mid(22,10).toULong();
+//   mStatus.setStatusFromStr( str );
+//   if (toUtf8) {
+//       msg->subject = str.mid(37, 100).trimmed();
+//       msg->from = str.mid(138, 50).trimmed();
+//       msg->to = str.mid(189, 50).trimmed();
+//   } else {
+//       start = offset = str.data() + 37;
+//       while (*start == ' ' && start - offset < 100) start++;
+//       msg->subject = QString::fromUtf8(str.mid(start - str.data(),
+//           100 - (start - offset)), 100 - (start - offset));
+//       start = offset = str.data() + 138;
+//       while (*start == ' ' && start - offset < 50) start++;
+//       msg->from = QString::fromUtf8(str.mid(start - str.data(),
+//           50 - (start - offset)), 50 - (start - offset));
+//       start = offset = str.data() + 189;
+//       while (*start == ' ' && start - offset < 50) start++;
+//       msg->to = QString::fromUtf8(str.mid(start - str.data(),
+//           50 - (start - offset)), 50 - (start - offset));
+//   }
+//   msg->replyToIdMD5 = str.mid(240, 22).trimmed();
+//   msg->msgIdMD5 = str.mid(263, 22).trimmed();
+    msg->mStatus.setStatusFromStr(QString::fromUtf8(str));
+    return true;
+}
+
+//-----------------------------------------------------------------------------
+
+static void swapEndian(QString &str)
+{
+    QChar *data = str.data();
+    while (!data->isNull()) {
+        *data = kmail_swap_16(data->unicode());
+        data++;
+    }
+}
+
+static int g_chunk_length = 0, g_chunk_offset = 0;
+static uchar *g_chunk = Q_NULLPTR;
+
+namespace
+{
+template < typename T > void copy_from_stream(T &x)
+{
+    if (g_chunk_offset + int(sizeof(T)) > g_chunk_length) {
+        g_chunk_offset = g_chunk_length;
+        qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "This should never happen..";
+        x = 0;
+    } else {
+        // the memcpy is optimized out by the compiler for the values
+        // of sizeof(T) that is called with
+        memcpy(&x, g_chunk + g_chunk_offset, sizeof(T));
+        g_chunk_offset += sizeof(T);
+    }
+}
+}
+
+bool KMIndexReader::fillPartsCache(KMIndexData *msg, off_t indexOff, short int indexLen)
+{
+    if (!msg) {
+        return false;
+    }
+    //qCDebug(MIXEDMAILDIR_LOG);
+    if (g_chunk_length < indexLen) {
+        g_chunk = (uchar *)realloc(g_chunk, g_chunk_length = indexLen);
+    }
+
+    off_t first_off = KDE_ftell(mFp);
+    KDE_fseek(mFp, indexOff, SEEK_SET);
+    if (fread(g_chunk, indexLen, readCount, mFp) != readCount) {
+        qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "Failed to read index";
+        return false;
+    }
+    KDE_fseek(mFp, first_off, SEEK_SET);
+
+    MsgPartType type;
+    quint16 len;
+    off_t ret = 0;
+    for (g_chunk_offset = 0; g_chunk_offset < indexLen; g_chunk_offset += len) {
+        quint32 tmp;
+        copy_from_stream(tmp);
+        copy_from_stream(len);
+        if (mIndexSwapByteOrder) {
+            tmp = kmail_swap_32(tmp);
+            len = kmail_swap_16(len);
+        }
+        type = (MsgPartType) tmp;
+        if (g_chunk_offset + len > indexLen) {
+            qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "g_chunk_offset + len > indexLen" << "This should never happen..";
+            return false;
+        }
+        // Only try to create strings if the part is really a string part, see declaration of
+        // MsgPartType
+        if (len && ((type >= 1 && type <= 6) || type == 11 || type == 14 || type == 15 || type == 19)) {
+
+            // This works because the QString constructor does a memcpy.
+            // Otherwise we would need to be concerned about the alignment.
+            QString str((QChar *)(g_chunk + g_chunk_offset), len / 2);
+            msg->mCachedStringParts[type] = str;
+
+            // Normally we need to swap the byte order because the QStrings are written
+            // in the style of Qt2 (MSB -> network ordered).
+            // QStrings in Qt3 expect host ordering.
+            // On e.g. Intel host ordering is LSB, on e.g. Sparc it is MSB.
+
+#     if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
+            // Byte order is little endian (swap is true)
+            swapEndian(msg->mCachedStringParts[type]);
+#     else
+            // Byte order is big endian (swap is false)
+#     endif
+        } else  if ((type >= 7 && type <= 10) || type == 12 || type == 13 || (type >= 16 && type <= 18)) {
+            Q_ASSERT(mIndexSizeOfLong == len);
+            if (mIndexSizeOfLong == sizeof(ret)) {
+                //qCDebug(MIXEDMAILDIR_LOG) << "mIndexSizeOfLong == sizeof(ret)";
+                // this memcpy replaces the original call to copy_from_stream
+                // so that g_chunk_offset is not changed
+                memcpy(&ret, g_chunk + g_chunk_offset, sizeof(ret));
+                if (mIndexSwapByteOrder) {
+                    if (sizeof(ret) == 4) {
+                        ret = kmail_swap_32(ret);
+                    } else {
+                        ret = kmail_swap_64(ret);
+                    }
+                }
+            }
+            //////////////////////
+            //BEGIN UNTESTED CODE
+            //////////////////////
+            else if (mIndexSizeOfLong == 4) {
+                // Long is stored as 4 bytes in index file, sizeof(long) = 8
+                quint32 ret_32;
+                // this memcpy replaces the original call to copy_from_stream
+                // so that g_chunk_offset is not changed
+                memcpy(&ret_32, g_chunk + g_chunk_offset, sizeof(quint32));
+                if (mIndexSwapByteOrder) {
+                    ret_32 = kmail_swap_32(ret_32);
+                }
+                ret = ret_32;
+            } else if (mIndexSizeOfLong == 8) {
+                // Long is stored as 8 bytes in index file, sizeof(long) = 4
+                quint32 ret_1;
+                quint32 ret_2;
+                // these memcpys replace the original calls to copy_from_stream
+                // so that g_chunk_offset is not changed
+                memcpy(&ret_1, g_chunk + g_chunk_offset, sizeof(quint32));
+                memcpy(&ret_2, g_chunk + g_chunk_offset, sizeof(quint32));
+                if (!mIndexSwapByteOrder) {
+                    // Index file order is the same as the order of this CPU.
+#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
+                    // Index file order is little endian
+                    ret = ret_1; // We drop the 4 most significant bytes
+#else
+                    // Index file order is big endian
+                    ret = ret_2; // We drop the 4 most significant bytes
+#endif
+                } else {
+                    // Index file order is different from this CPU.
+#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
+                    // Index file order is big endian
+                    ret = ret_2; // We drop the 4 most significant bytes
+#else
+                    // Index file order is little endian
+                    ret = ret_1; // We drop the 4 most significant bytes
+#endif
+                    // We swap the result to host order.
+                    ret = kmail_swap_32(ret);
+                }
+            }
+            //////////////////////
+            //END UNTESTED CODE
+            //////////////////////
+            msg->mCachedLongParts[type] = ret;
+        }
+    } // for loop
+    msg->mPartsCacheBuilt = true;
+    return true;
+}
+
+//-----------------------------------------------------------------------------
+
+QList< KMIndexDataPtr > KMIndexReader::messages()
+{
+    return mMsgList;
+}
diff --git a/resources/mixedmaildir/kmindexreader/kmindexreader.h b/resources/mixedmaildir/kmindexreader/kmindexreader.h
new file mode 100644 (file)
index 0000000..9c325b8
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+ *   Copyright (C) 2010 Casey Link <unnamedrambler@gmail.com>
+ *   Copyright (c) 2009-2010 Klaralvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License along
+ *   with this program; if not, write to the Free Software Foundation, Inc.,
+ *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ */
+
+#ifndef KMINDEXREADER_H
+#define KMINDEXREADER_H
+
+#include "kmindexreader_export.h"
+
+#include <Akonadi/KMime/MessageStatus>
+using Akonadi::MessageStatus;
+
+#include <QString>
+#include <QStringList>
+#include <QFile>
+#include <QList>
+#include <QSharedPointer>
+
+#include <ctype.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+class KMINDEXREADER_EXPORT KMIndexData
+{
+    Q_DISABLE_COPY(KMIndexData)
+public:
+    KMIndexData();
+    /** Status object of the message. */
+    MessageStatus &status();
+    QStringList  tagList() const;
+    quint64 uid() const;
+    bool isEmpty() const;
+
+private:
+    QString mCachedStringParts[20];
+    unsigned long mCachedLongParts[20];
+    bool mPartsCacheBuilt;
+
+    MessageStatus mStatus;
+    friend class KMIndexReader;
+    friend class TestIdxReader;
+};
+
+typedef QSharedPointer<KMIndexData> KMIndexDataPtr;
+
+/**
+ * @short A class to read legacy kmail (< 4.5) index files
+ *
+ * This class provides read-only access to legacy kmail index files.
+ * It uses old kmfolderindex code, authors attributed as appropriate.
+ * @author Casey Link <unnamedrambler@gmail.com>
+ */
+class KMINDEXREADER_EXPORT KMIndexReader
+{
+public:
+    explicit KMIndexReader(const QString &indexFile);
+    ~KMIndexReader();
+
+    bool error() const;
+
+    /**
+     * begins the index reading process
+     */
+    bool readIndex();
+
+    enum MsgPartType {
+        MsgNoPart = 0,
+        //unicode strings
+        MsgFromPart = 1,
+        MsgSubjectPart = 2,
+        MsgToPart = 3,
+        MsgReplyToIdMD5Part = 4,
+        MsgIdMD5Part = 5,
+        MsgXMarkPart = 6,
+        //unsigned long
+        MsgOffsetPart = 7,
+        MsgLegacyStatusPart = 8,
+        MsgSizePart = 9,
+        MsgDatePart = 10,
+        // unicode string
+        MsgFilePart = 11,
+        // unsigned long
+        MsgCryptoStatePart = 12,
+        MsgMDNSentPart = 13,
+        //another two unicode strings
+        MsgReplyToAuxIdMD5Part = 14,
+        MsgStrippedSubjectMD5Part = 15,
+        // and another unsigned long
+        MsgStatusPart = 16,
+        MsgSizeServerPart = 17,
+        MsgUIDPart = 18,
+        // unicode string
+        MsgTagPart = 19
+    };
+
+    KMIndexDataPtr dataByOffset(quint64 offset) const;
+
+    KMIndexDataPtr dataByFileName(const QString &fileName) const;
+
+private:
+
+    /**
+     * Reads the header of an index
+     */
+    bool readHeader(int *version);
+
+    /**
+     * creates a message object from an old index files
+     */
+    bool fromOldIndexString(KMIndexData *msg, const QByteArray &str, bool toUtf8);
+
+    bool fillPartsCache(KMIndexData *msg, off_t off, short int len);
+
+    QList<KMIndexDataPtr> messages();
+
+    QString mIndexFileName;
+    QFile mIndexFile;
+    FILE *mFp;
+
+    bool mConvertToUtf8;
+    bool mIndexSwapByteOrder; // Index file was written with swapped byte order
+    int mIndexSizeOfLong; // Index file was written with longs of this size
+    off_t mHeaderOffset;
+
+    bool mError;
+
+    /** list of index entries or messages */
+    QList<KMIndexDataPtr> mMsgList;
+    QHash<QString, KMIndexDataPtr> mMsgByFileName;
+    QHash<quint64, KMIndexDataPtr> mMsgByOffset;
+    friend class TestIdxReader;
+};
+
+#endif // KMINDEXREADER_H
diff --git a/resources/mixedmaildir/mixedmaildir_debug.cpp b/resources/mixedmaildir/mixedmaildir_debug.cpp
new file mode 100644 (file)
index 0000000..d6db4a0
--- /dev/null
@@ -0,0 +1,22 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2014 Laurent Montel <montel@kde.org>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "mixedmaildir_debug.h"
+Q_LOGGING_CATEGORY(MIXEDMAILDIR_LOG, "log_mixedmaildir")
+
diff --git a/resources/mixedmaildir/mixedmaildir_debug.h b/resources/mixedmaildir/mixedmaildir_debug.h
new file mode 100644 (file)
index 0000000..1549c6e
--- /dev/null
@@ -0,0 +1,27 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2014 Laurent Montel <montel@kde.org>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef MIXEDMAILDIR_DEBUG_H
+#define MIXEDMAILDIR_DEBUG_H
+
+#include <QLoggingCategory>
+Q_DECLARE_LOGGING_CATEGORY(MIXEDMAILDIR_LOG)
+
+#endif
+
diff --git a/resources/mixedmaildir/mixedmaildirresource.cpp b/resources/mixedmaildir/mixedmaildirresource.cpp
new file mode 100644 (file)
index 0000000..27fb759
--- /dev/null
@@ -0,0 +1,831 @@
+/*  This file is part of the KDE project
+    Copyright (c) 2007 Till Adam <adam@kde.org>
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+    Copyright (C) 2011 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "mixedmaildirresource.h"
+#include "mixedmaildir_debug.h"
+
+#include "compactchangehelper.h"
+#include "configdialog.h"
+#include "mixedmaildirstore.h"
+#include "settings.h"
+#include "settingsadaptor.h"
+#include "retrieveitemsjob.h"
+#include "createandsettagsjob.h"
+
+#include "filestore/collectioncreatejob.h"
+#include "filestore/collectiondeletejob.h"
+#include "filestore/collectionfetchjob.h"
+#include "filestore/collectionmodifyjob.h"
+#include "filestore/collectionmovejob.h"
+#include "filestore/entitycompactchangeattribute.h"
+#include "filestore/itemcreatejob.h"
+#include "filestore/itemdeletejob.h"
+#include "filestore/itemfetchjob.h"
+#include "filestore/itemmodifyjob.h"
+#include "filestore/itemmovejob.h"
+#include "filestore/storecompactjob.h"
+
+#include <akonadi/kmime/messageparts.h>
+#include <akonadi/kmime/messagestatus.h>
+
+#include <AkonadiCore/changerecorder.h>
+#include <AkonadiCore/itemfetchjob.h>
+#include <AkonadiCore/itemfetchscope.h>
+#include <AkonadiCore/itemmodifyjob.h>
+#include <AkonadiCore/collectionfetchscope.h>
+
+#include <kmime/kmime_message.h>
+
+#include "mixedmaildirresource_debug.h"
+#include <KLocalizedString>
+#include <KWindowSystem>
+
+#include <QtCore/QDir>
+#include <QtDBus/QDBusConnection>
+
+#include <AkonadiCore/Tag>
+
+using namespace Akonadi;
+
+MixedMaildirResource::MixedMaildirResource(const QString &id)
+    : ResourceBase(id), mStore(new MixedMaildirStore()), mCompactHelper(Q_NULLPTR)
+{
+    new SettingsAdaptor(Settings::self());
+    QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"),
+            Settings::self(), QDBusConnection::ExportAdaptors);
+    connect(this, &MixedMaildirResource::reloadConfiguration, this, &MixedMaildirResource::reapplyConfiguration);
+
+    // We need to enable this here, otherwise we neither get the remote ID of the
+    // parent collection when a collection changes, nor the full item when an item
+    // is added.
+    changeRecorder()->fetchCollection(true);
+    changeRecorder()->itemFetchScope().fetchFullPayload(true);
+    changeRecorder()->itemFetchScope().setAncestorRetrieval(ItemFetchScope::All);
+    changeRecorder()->collectionFetchScope().setAncestorRetrieval(CollectionFetchScope::All);
+
+    setHierarchicalRemoteIdentifiersEnabled(true);
+
+    if (ensureSaneConfiguration()) {
+        const bool changeName = name().isEmpty() || name() == identifier() ||
+                                name() == mStore->topLevelCollection().name();
+        mStore->setPath(Settings::self()->path());
+        if (changeName) {
+            setName(mStore->topLevelCollection().name());
+        }
+    }
+
+    const QByteArray compactHelperSessionId = id.toUtf8() + "-compacthelper";
+    mCompactHelper = new CompactChangeHelper(compactHelperSessionId, this);
+}
+
+MixedMaildirResource::~MixedMaildirResource()
+{
+    delete mStore;
+}
+
+void MixedMaildirResource::aboutToQuit()
+{
+    // The settings may not have been saved if e.g. they have been modified via
+    // DBus instead of the config dialog.
+    Settings::self()->save();
+}
+
+void MixedMaildirResource::configure(WId windowId)
+{
+    ConfigDialog dlg;
+    if (windowId) {
+        KWindowSystem::setMainWindow(&dlg, windowId);
+    }
+    dlg.setWindowIcon(QIcon::fromTheme(QStringLiteral("message-rfc822")));
+
+    bool fullSync = false;
+
+    if (dlg.exec()) {
+        const bool changeName = name().isEmpty() || name() == identifier() ||
+                                name() == mStore->topLevelCollection().name();
+
+        const QString oldPath = mStore->path();
+        mStore->setPath(Settings::self()->path());
+
+        fullSync = oldPath != mStore->path();
+
+        if (changeName) {
+            setName(mStore->topLevelCollection().name());
+        }
+        Q_EMIT configurationDialogAccepted();
+    } else {
+        Q_EMIT configurationDialogRejected();
+    }
+
+    if (ensureDirExists()) {
+        if (fullSync) {
+            mSynchronizedCollections.clear();
+            mPendingSynchronizeCollections.clear();
+        }
+        synchronizeCollectionTree();
+    }
+}
+
+void MixedMaildirResource::itemAdded(const Item &item, const Collection &collection)
+{
+    /*  qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "item.id=" << item.id() << "col=" << collection.remoteId();*/
+    if (!ensureSaneConfiguration()) {
+        const QString message = i18nc("@info:status", "Unusable configuration.");
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << message;
+        cancelTask(message);
+        return;
+    }
+
+    FileStore::ItemCreateJob *job = mStore->createItem(item, collection);
+    connect(job, &FileStore::ItemCreateJob::result, this, &MixedMaildirResource::itemAddedResult);
+}
+
+void MixedMaildirResource::itemChanged(const Item &item, const QSet<QByteArray> &parts)
+{
+    /*  qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "item.id=" << item.id() << "col=" << item.parentCollection().remoteId()
+               << "parts=" << parts;*/
+    if (!ensureSaneConfiguration()) {
+        const QString message = i18nc("@info:status", "Unusable configuration.");
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << message;
+        cancelTask(message);
+        return;
+    }
+
+    if (Settings::self()->readOnly()) {
+        changeProcessed();
+        return;
+    }
+
+    Item storeItem(item);
+    storeItem.setRemoteId(mCompactHelper->currentRemoteId(item));
+
+    FileStore::ItemModifyJob *job = mStore->modifyItem(storeItem);
+    job->setIgnorePayload(!item.hasPayload<KMime::Message::Ptr>());
+    job->setParts(parts);
+    job->setProperty("originalRemoteId", storeItem.remoteId());
+    connect(job, &FileStore::ItemModifyJob::result, this, &MixedMaildirResource::itemChangedResult);
+}
+
+void MixedMaildirResource::itemMoved(const Item &item, const Collection &source, const Collection &destination)
+{
+    /*  qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "item.id=" << item.id() << "remoteId=" << item.remoteId()
+               << "source=" << source.remoteId() << "dest=" << destination.remoteId();*/
+    if (source == destination) {
+        changeProcessed();
+        return;
+    }
+
+    if (!ensureSaneConfiguration()) {
+        const QString message = i18nc("@info:status", "Unusable configuration.");
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << message;
+        cancelTask(message);
+        return;
+    }
+
+    Item moveItem = item;
+    moveItem.setRemoteId(mCompactHelper->currentRemoteId(item));
+    moveItem.setParentCollection(source);
+
+    FileStore::ItemMoveJob *job = mStore->moveItem(moveItem, destination);
+    job->setProperty("originalRemoteId", moveItem.remoteId());
+    connect(job, &FileStore::ItemMoveJob::result, this, &MixedMaildirResource::itemMovedResult);
+}
+
+void MixedMaildirResource::itemRemoved(const Item &item)
+{
+    /*  qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "item.id=" << item.id() << "col=" << collection.remoteId()
+               << "collection.remoteRevision=" << item.parentCollection().remoteRevision();*/
+    Q_ASSERT(!item.remoteId().isEmpty());
+    Q_ASSERT(item.parentCollection().isValid());
+    if (item.parentCollection().remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Item %1 belongs to invalid collection %2. Maybe it was deleted meanwhile?", item.id(), item.parentCollection().id());
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << message;
+        cancelTask(message);
+        return;
+    }
+
+    if (!ensureSaneConfiguration()) {
+        const QString message = i18nc("@info:status", "Unusable configuration.");
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << message;
+        cancelTask(message);
+        return;
+    }
+
+    Item storeItem(item);
+    storeItem.setRemoteId(mCompactHelper->currentRemoteId(item));
+    FileStore::ItemDeleteJob *job = mStore->deleteItem(storeItem);
+    connect(job, &FileStore::ItemDeleteJob::result, this, &MixedMaildirResource::itemRemovedResult);
+}
+
+void MixedMaildirResource::retrieveCollections()
+{
+    if (!ensureSaneConfiguration()) {
+        const QString message = i18nc("@info:status", "Unusable configuration.");
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << message;
+        cancelTask(message);
+        return;
+    }
+
+    FileStore::CollectionFetchJob *job = mStore->fetchCollections(mStore->topLevelCollection(), FileStore::CollectionFetchJob::Recursive);
+    connect(job, &FileStore::CollectionFetchJob::result, this, &MixedMaildirResource::retrieveCollectionsResult);
+
+    status(Running, i18nc("@info:status", "Synchronizing email folders"));
+}
+
+void MixedMaildirResource::retrieveItems(const Collection &col)
+{
+    if (!ensureSaneConfiguration()) {
+        const QString message = i18nc("@info:status", "Unusable configuration.");
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << message;
+        cancelTask(message);
+        return;
+    }
+
+    RetrieveItemsJob *job = new RetrieveItemsJob(col, mStore, this);
+    connect(job, &RetrieveItemsJob::result, this, &MixedMaildirResource::retrieveItemsResult);
+
+    status(Running, i18nc("@info:status", "Synchronizing email folder %1", col.name()));
+}
+
+bool MixedMaildirResource::retrieveItem(const Item &item, const QSet<QByteArray> &parts)
+{
+    Q_UNUSED(parts);
+
+    FileStore::ItemFetchJob *job = mStore->fetchItem(item);
+    if (parts.contains(Item::FullPayload)) {
+        job->fetchScope().fetchFullPayload(true);
+    } else {
+        Q_FOREACH (const QByteArray &part, parts) {
+            job->fetchScope().fetchPayloadPart(part, true);
+        }
+    }
+    connect(job, &RetrieveItemsJob::result, this, &MixedMaildirResource::retrieveItemResult);
+
+    return true;
+}
+
+void MixedMaildirResource::collectionAdded(const Collection &collection, const Collection &parent)
+{
+    if (!ensureSaneConfiguration()) {
+        const QString message = i18nc("@info:status", "Unusable configuration.");
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << message;
+        cancelTask(message);
+        return;
+    }
+
+    FileStore::CollectionCreateJob *job = mStore->createCollection(collection, parent);
+    connect(job, &RetrieveItemsJob::result, this, &MixedMaildirResource::collectionAddedResult);
+}
+
+void MixedMaildirResource::collectionChanged(const Collection &collection)
+{
+    if (!ensureSaneConfiguration()) {
+        const QString message = i18nc("@info:status", "Unusable configuration.");
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << message;
+        cancelTask(message);
+        return;
+    }
+
+    // when the top level collection gets renamed, we do not rename the directory
+    // but rename the resource.
+    if (collection.remoteId() == mStore->topLevelCollection().remoteId()) {
+        if (collection.name() != name()) {
+            qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "TopLevel collection name differs from resource name: collection="
+                                              << collection.name() << "resource=" << name() << ". Renaming resource";
+            setName(collection.name());
+        }
+        changeCommitted(collection);
+        return;
+    }
+
+    mCompactHelper->checkCollectionChanged(collection);
+
+    FileStore::CollectionModifyJob *job = mStore->modifyCollection(collection);
+    connect(job, &RetrieveItemsJob::result, this, &MixedMaildirResource::collectionChangedResult);
+}
+
+void MixedMaildirResource::collectionChanged(const Collection &collection, const QSet<QByteArray> &changedAttributes)
+{
+    if (!ensureSaneConfiguration()) {
+        const QString message = i18nc("@info:status", "Unusable configuration.");
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << message;
+        cancelTask(message);
+        return;
+    }
+
+    // when the top level collection gets renamed, we do not rename the directory
+    // but rename the resource.
+    if (collection.remoteId() == mStore->topLevelCollection().remoteId()) {
+        if (collection.name() != name()) {
+            qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "TopLevel collection name differs from resource name: collection="
+                                              << collection.name() << "resource=" << name() << ". Renaming resource";
+            setName(collection.name());
+        }
+        changeCommitted(collection);
+        return;
+    }
+
+    mCompactHelper->checkCollectionChanged(collection);
+
+    Q_UNUSED(changedAttributes);
+
+    FileStore::CollectionModifyJob *job = mStore->modifyCollection(collection);
+    connect(job, &RetrieveItemsJob::result, this, &MixedMaildirResource::collectionChangedResult);
+}
+
+void MixedMaildirResource::collectionMoved(const Collection &collection, const Collection &source, const Collection &dest)
+{
+    //qCDebug(MIXEDMAILDIR_LOG) << collection << source << dest;
+
+    if (!ensureSaneConfiguration()) {
+        const QString message = i18nc("@info:status", "Unusable configuration.");
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << message;
+        cancelTask(message);
+        return;
+    }
+
+    if (collection.parentCollection() == Collection::root()) {
+        const QString message = i18nc("@info:status", "Cannot move root maildir folder '%1'.", collection.remoteId());
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << message;
+        cancelTask(message);
+        return;
+    }
+
+    if (source == dest) {   // should not happen, but who knows...
+        changeProcessed();
+        return;
+    }
+
+    Collection moveCollection = collection;
+    moveCollection.setParentCollection(source);
+
+    FileStore::CollectionMoveJob *job = mStore->moveCollection(moveCollection, dest);
+    connect(job, &RetrieveItemsJob::result, this, &MixedMaildirResource::collectionMovedResult);
+}
+
+void MixedMaildirResource::collectionRemoved(const Collection &collection)
+{
+    if (!ensureSaneConfiguration()) {
+        const QString message = i18nc("@info:status", "Unusable configuration.");
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << message;
+        cancelTask(message);
+        return;
+    }
+
+    if (collection.parentCollection() == Collection::root()) {
+        Q_EMIT error(i18n("Cannot delete top-level maildir folder '%1'.", Settings::self()->path()));
+        changeProcessed();
+        return;
+    }
+
+    FileStore::CollectionDeleteJob *job = mStore->deleteCollection(collection);
+    connect(job, &RetrieveItemsJob::result, this, &MixedMaildirResource::collectionRemovedResult);
+}
+
+bool MixedMaildirResource::ensureDirExists()
+{
+    QDir dir(Settings::self()->path());
+    if (!dir.exists()) {
+        if (!dir.mkpath(Settings::self()->path())) {
+            const QString message = i18nc("@info:status", "Unable to create maildir '%1'.", Settings::self()->path());
+            qCCritical(MIXEDMAILDIRRESOURCE_LOG) << message;
+            status(Broken, message);
+            return false;
+        }
+    }
+    return true;
+}
+
+bool MixedMaildirResource::ensureSaneConfiguration()
+{
+    if (Settings::self()->path().isEmpty()) {
+        const QString message = i18nc("@info:status", "No usable storage location configured.");
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << message;
+        status(NotConfigured, message);
+        return false;
+    }
+    return true;
+}
+
+void MixedMaildirResource::checkForInvalidatedIndexCollections(KJob *job)
+{
+    // when operations invalidate the on-disk index, we need to make sure all index
+    // data has been transferred into Akonadi by synchronizing the collections
+    const QVariant var = job->property("onDiskIndexInvalidated");
+    if (var.isValid()) {
+        const Collection::List collections = var.value<Collection::List>();
+        qCDebug(MIXEDMAILDIR_LOG) << "On disk index of" << collections.count()
+                                  << "collections invalidated after" << job->metaObject()->className();
+
+        Q_FOREACH (const Collection &collection, collections) {
+            const Collection::Id id = collection.id();
+            if (!mSynchronizedCollections.contains(id) && !mPendingSynchronizeCollections.contains(id)) {
+                qCDebug(MIXEDMAILDIR_LOG) << "Requesting sync of collection" << collection.name()
+                                          << ", id=" << collection.id();
+                mPendingSynchronizeCollections << id;
+                synchronizeCollection(id);
+            }
+        }
+    }
+}
+
+void MixedMaildirResource::reapplyConfiguration()
+{
+    if (ensureSaneConfiguration() && ensureDirExists()) {
+        const QString oldPath = mStore->path();
+        mStore->setPath(Settings::self()->path());
+
+        if (oldPath != mStore->path()) {
+            mSynchronizedCollections.clear();
+            mPendingSynchronizeCollections.clear();
+        }
+        synchronizeCollectionTree();
+    }
+}
+
+void MixedMaildirResource::retrieveCollectionsResult(KJob *job)
+{
+    if (job->error() != 0) {
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << job->errorString();
+        status(Broken, job->errorString());
+        cancelTask(job->errorString());
+        return;
+    }
+
+    FileStore::CollectionFetchJob *fetchJob = qobject_cast<FileStore::CollectionFetchJob *>(job);
+    Q_ASSERT(fetchJob != 0);
+
+    Collection topLevelCollection = mStore->topLevelCollection();
+    if (!name().isEmpty() && name() != identifier()) {
+        topLevelCollection.setName(name());
+    }
+
+    Collection::List collections;
+    collections << topLevelCollection;
+    collections << fetchJob->collections();
+    collectionsRetrieved(collections);
+}
+
+void MixedMaildirResource::retrieveItemsResult(KJob *job)
+{
+    if (job->error() != 0) {
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << job->errorString();
+        status(Broken, job->errorString());
+        cancelTask(job->errorString());
+        return;
+    }
+
+    RetrieveItemsJob *retrieveJob = qobject_cast<RetrieveItemsJob *>(job);
+    Q_ASSERT(retrieveJob != 0);
+
+    // messages marked as deleted have been deleted from mbox files but never got purged
+    // TODO FileStore could provide deleteItems() to deleted all filtered items in one go
+    KJob *deleteJob = Q_NULLPTR;
+    qCDebug(MIXEDMAILDIR_LOG) << retrieveJob->itemsMarkedAsDeleted().count()
+                              << "items marked as Deleted";
+    Q_FOREACH (const Item &item, retrieveJob->itemsMarkedAsDeleted()) {
+        deleteJob = mStore->deleteItem(item);
+    }
+
+    if (deleteJob != Q_NULLPTR) {
+        // last item delete triggers mbox purge, i.e. store compact
+        const bool connected = connect(deleteJob, &KJob::result, this, &MixedMaildirResource::itemsDeleted);
+        Q_ASSERT(connected);
+        Q_UNUSED(connected);
+    }
+
+    // if some items have tags, we need to complete the retrieval and schedule tagging
+    // to a later time so we can then fetch the items to get their Akonadi URLs
+    const Item::List items = retrieveJob->availableItems();
+    const QVariant var = retrieveJob->property("remoteIdToTagList");
+    if (var.isValid()) {
+        const QHash<QString, QVariant> tagListHash = var.value< QHash<QString, QVariant> >();
+        if (!tagListHash.isEmpty()) {
+            qCDebug(MIXEDMAILDIRRESOURCE_LOG) << tagListHash.count() << "of" << items.count()
+                                              << "items in collection" << retrieveJob->collection().remoteId() << "have tags";
+
+            TagContextList taggedItems;
+            Q_FOREACH (const Item &item, items) {
+                const QVariant tagListVar = tagListHash[ item.remoteId() ];
+                if (tagListVar.isValid()) {
+                    const QStringList tagList = tagListVar.value<QStringList>();
+                    if (!tagListHash.isEmpty()) {
+                        TagContext tag;
+                        tag.mItem = item;
+                        tag.mTagList = tagList;
+
+                        taggedItems << tag;
+                    }
+                }
+            }
+
+            if (!taggedItems.isEmpty()) {
+                mTagContextByColId.insert(retrieveJob->collection().id(), taggedItems);
+
+                scheduleCustomTask(this, "restoreTags", QVariant::fromValue<Collection>(retrieveJob->collection()));
+            }
+        }
+    }
+
+    mSynchronizedCollections << retrieveJob->collection().id();
+    mPendingSynchronizeCollections.remove(retrieveJob->collection().id());
+
+    itemsRetrievalDone();
+}
+
+void MixedMaildirResource::retrieveItemResult(KJob *job)
+{
+    if (job->error() != 0) {
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << job->errorString();
+        status(Broken, job->errorString());
+        cancelTask(job->errorString());
+        return;
+    }
+
+    FileStore::ItemFetchJob *fetchJob = qobject_cast<FileStore::ItemFetchJob *>(job);
+    Q_ASSERT(fetchJob != 0);
+    Q_ASSERT(!fetchJob->items().isEmpty());
+
+    itemRetrieved(fetchJob->items().at(0));
+}
+
+void MixedMaildirResource::itemAddedResult(KJob *job)
+{
+    if (job->error() != 0) {
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << job->errorString();
+        status(Broken, job->errorString());
+        cancelTask(job->errorString());
+        return;
+    }
+
+    FileStore::ItemCreateJob *itemJob = qobject_cast<FileStore::ItemCreateJob *>(job);
+    Q_ASSERT(itemJob != 0);
+
+    /*  qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "item.id=" << itemJob->item().id() << "remoteId=" << itemJob->item().remoteId();*/
+    changeCommitted(itemJob->item());
+
+    checkForInvalidatedIndexCollections(job);
+}
+
+void MixedMaildirResource::itemChangedResult(KJob *job)
+{
+    if (job->error() != 0) {
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << job->errorString();
+        status(Broken, job->errorString());
+        cancelTask(job->errorString());
+        return;
+    }
+
+    FileStore::ItemModifyJob *itemJob = qobject_cast<FileStore::ItemModifyJob *>(job);
+    Q_ASSERT(itemJob != 0);
+
+    changeCommitted(itemJob->item());
+
+    const QString remoteId = itemJob->property("originalRemoteId").value<QString>();
+
+    const QVariant compactStoreVar = itemJob->property("compactStore");
+    if (compactStoreVar.isValid() && compactStoreVar.toBool()) {
+        scheduleCustomTask(this, "compactStore", QVariant());
+    }
+
+    checkForInvalidatedIndexCollections(job);
+}
+
+void MixedMaildirResource::itemMovedResult(KJob *job)
+{
+    if (job->error() != 0) {
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << job->errorString();
+        status(Broken, job->errorString());
+        cancelTask(job->errorString());
+        return;
+    }
+
+    FileStore::ItemMoveJob *itemJob = qobject_cast<FileStore::ItemMoveJob *>(job);
+    Q_ASSERT(itemJob != 0);
+
+    changeCommitted(itemJob->item());
+
+    const QString remoteId = itemJob->property("originalRemoteId").value<QString>();
+//   qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "item.id=" << itemJob->item().id() << "remoteId=" << itemJob->item().remoteId()
+//            << "old remoteId=" << remoteId;
+
+    const QVariant compactStoreVar = itemJob->property("compactStore");
+    if (compactStoreVar.isValid() && compactStoreVar.toBool()) {
+        scheduleCustomTask(this, "compactStore", QVariant());
+    }
+
+    checkForInvalidatedIndexCollections(job);
+}
+
+void MixedMaildirResource::itemRemovedResult(KJob *job)
+{
+    if (job->error() != 0) {
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << job->errorString();
+        status(Broken, job->errorString());
+        cancelTask(job->errorString());
+        return;
+    }
+
+    FileStore::ItemDeleteJob *itemJob = qobject_cast<FileStore::ItemDeleteJob *>(job);
+    Q_ASSERT(itemJob != 0);
+
+    changeCommitted(itemJob->item());
+
+    const QString remoteId = itemJob->item().remoteId();
+
+    const QVariant compactStoreVar = itemJob->property("compactStore");
+    if (compactStoreVar.isValid() && compactStoreVar.toBool()) {
+        scheduleCustomTask(this, "compactStore", QVariant());
+    }
+
+    checkForInvalidatedIndexCollections(job);
+}
+
+void MixedMaildirResource::itemsDeleted(KJob *job)
+{
+    Q_UNUSED(job);
+    scheduleCustomTask(this, "compactStore", QVariant());
+}
+
+void MixedMaildirResource::collectionAddedResult(KJob *job)
+{
+    if (job->error() != 0) {
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << job->errorString();
+        status(Broken, job->errorString());
+        cancelTask(job->errorString());
+        return;
+    }
+
+    FileStore::CollectionCreateJob *colJob = qobject_cast<FileStore::CollectionCreateJob *>(job);
+    Q_ASSERT(colJob != 0);
+
+    changeCommitted(colJob->collection());
+}
+
+void MixedMaildirResource::collectionChangedResult(KJob *job)
+{
+    if (job->error() != 0) {
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << job->errorString();
+        status(Broken, job->errorString());
+        cancelTask(job->errorString());
+        return;
+    }
+
+    FileStore::CollectionModifyJob *colJob = qobject_cast<FileStore::CollectionModifyJob *>(job);
+    Q_ASSERT(colJob != 0);
+
+    changeCommitted(colJob->collection());
+
+    checkForInvalidatedIndexCollections(job);
+}
+
+void MixedMaildirResource::collectionMovedResult(KJob *job)
+{
+    if (job->error() != 0) {
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << job->errorString();
+        status(Broken, job->errorString());
+        cancelTask(job->errorString());
+        return;
+    }
+
+    FileStore::CollectionMoveJob *colJob = qobject_cast<FileStore::CollectionMoveJob *>(job);
+    Q_ASSERT(colJob != 0);
+
+    changeCommitted(colJob->collection());
+
+    checkForInvalidatedIndexCollections(job);
+}
+
+void MixedMaildirResource::collectionRemovedResult(KJob *job)
+{
+    if (job->error() != 0) {
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << job->errorString();
+        status(Broken, job->errorString());
+        cancelTask(job->errorString());
+        return;
+    }
+
+    FileStore::CollectionDeleteJob *colJob = qobject_cast<FileStore::CollectionDeleteJob *>(job);
+    Q_ASSERT(colJob != 0);
+
+    changeCommitted(colJob->collection());
+}
+
+void MixedMaildirResource::compactStore(const QVariant &arg)
+{
+    Q_UNUSED(arg);
+
+    FileStore::StoreCompactJob *job = mStore->compactStore();
+    connect(job, &RetrieveItemsJob::result, this, &MixedMaildirResource::compactStoreResult);
+}
+
+void MixedMaildirResource::compactStoreResult(KJob *job)
+{
+    if (job->error() != 0) {
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << job->errorString();
+        status(Broken, job->errorString());
+        cancelTask(job->errorString());
+        return;
+    }
+
+    FileStore::StoreCompactJob *compactJob = qobject_cast<FileStore::StoreCompactJob *>(job);
+    Q_ASSERT(compactJob != 0);
+
+    const Item::List items = compactJob->changedItems();
+    qCDebug(MIXEDMAILDIR_LOG) << "Compacting store resulted in" << items.count() << "changed items";
+
+    mCompactHelper->addChangedItems(items);
+
+    taskDone();
+
+    checkForInvalidatedIndexCollections(job);
+}
+
+void MixedMaildirResource::restoreTags(const QVariant &arg)
+{
+    if (!arg.isValid()) {
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << "Given variant is not valid";
+        cancelTask();
+        return;
+    }
+
+    const Collection collection = arg.value<Collection>();
+    if (!collection.isValid()) {
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << "Given variant is not valid";
+        cancelTask();
+        return;
+    }
+
+    const TagContextList taggedItems = mTagContextByColId[ collection.id() ];
+    mPendingTagContexts << taggedItems;
+
+    QMetaObject::invokeMethod(this, "processNextTagContext", Qt::QueuedConnection);
+    taskDone();
+}
+
+void MixedMaildirResource::processNextTagContext()
+{
+    qCDebug(MIXEDMAILDIRRESOURCE_LOG) << mPendingTagContexts.count() << "items to go";
+    if (mPendingTagContexts.isEmpty()) {
+        return;
+    }
+
+    const TagContext tagContext = mPendingTagContexts.front();
+    mPendingTagContexts.pop_front();
+
+    ItemFetchJob *fetchJob = new ItemFetchJob(tagContext.mItem);
+    fetchJob->setProperty("tagList", tagContext.mTagList);
+    connect(fetchJob, &ItemFetchJob::result, this, &MixedMaildirResource::tagFetchJobResult);
+}
+
+void MixedMaildirResource::tagFetchJobResult(KJob *job)
+{
+    if (job->error() != 0) {
+        qCCritical(MIXEDMAILDIRRESOURCE_LOG) << job->errorString();
+        processNextTagContext();
+        return;
+    }
+
+    ItemFetchJob *fetchJob = qobject_cast<ItemFetchJob *>(job);
+    Q_ASSERT(fetchJob != 0);
+
+    Q_ASSERT(!fetchJob->items().isEmpty());
+
+    const Item item = fetchJob->items().at(0);
+    const QStringList tagList = job->property("tagList").value<QStringList>();
+    qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "Tagging item" << item.url() << "with" << tagList;
+
+    Akonadi::Tag::List tags;
+    Q_FOREACH (const QString &tag, tagList) {
+        if (tag.isEmpty()) {
+            qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "TagList for item" << item.url() << "contains an empty tag";
+        } else {
+            tags << Akonadi::Tag(tag);
+        }
+    }
+    new CreateAndSetTagsJob(item, tags);
+
+    processNextTagContext();
+}
+
+AKONADI_RESOURCE_MAIN(MixedMaildirResource)
+
diff --git a/resources/mixedmaildir/mixedmaildirresource.desktop b/resources/mixedmaildir/mixedmaildirresource.desktop
new file mode 100644 (file)
index 0000000..663c4e3
--- /dev/null
@@ -0,0 +1,95 @@
+[Desktop Entry]
+Name=KMail Mail Folder
+Name[bg]=Папка с поща на KMail
+Name[bs]=KMail fascikla pošte
+Name[ca]=Carpeta de correu del KMail
+Name[ca@valencia]=Carpeta de correu del KMail
+Name[cs]=Složka pošty KMailu
+Name[da]=KMail's Mail-mappe
+Name[de]=KMail-Mailordner
+Name[el]=Φάκελος αλληλογραφίας KMail
+Name[en_GB]=KMail Mail Folder
+Name[es]=Carpeta de correo de KMail
+Name[et]=KMaili kirjade kataloog
+Name[fi]=KMail-postikansio
+Name[fr]=Dossier de courriers électroniques KMail
+Name[ga]=Fillteán Ríomhphoist KMail
+Name[gl]=Cartafol de correo de KMail
+Name[hu]=KMail levélmappa
+Name[ia]=Dossier de posta de KMail
+Name[it]=Cartella di posta di KMail
+Name[kk]=KMail пошта қапшығы
+Name[km]=ថត​សំបុត្រ KMail
+Name[ko]=KMail 메일 폴더
+Name[lt]=KMail el. laiškų katalogas
+Name[lv]=KMail pasta mape
+Name[nb]=KMail e-postmappe
+Name[nds]=KMail-Nettpostorner
+Name[nl]=E-mailmap van KMail
+Name[pl]=Katalog Poczty KMail
+Name[pt]=Pasta de Correio do KMail
+Name[pt_BR]=Pasta de e-mails do KMail
+Name[ru]=Почтовая папка KMail
+Name[sk]=Poštový priečinok KMail
+Name[sl]=Poštna mapa za KMail
+Name[sr]=К‑поштина поштанска фасцикла
+Name[sr@ijekavian]=К‑поштина поштанска фасцикла
+Name[sr@ijekavianlatin]=K‑poština poštanska fascikla
+Name[sr@latin]=K‑poština poštanska fascikla
+Name[sv]=Kmail brevkatalog
+Name[tr]=KMail Mail Dizini
+Name[uk]=Тека пошти KMail
+Name[x-test]=xxKMail Mail Folderxx
+Name[zh_CN]=KMail 邮件文件夹
+Name[zh_TW]=KMail 資料夾
+Comment=Loads data from a local KMail mail folder
+Comment[bg]=Зареждане на данни от локална папка с поща на KMail
+Comment[bs]=Učitava podatke iz lokalne fascikle KMail pošte
+Comment[ca]=Carrega les dades des d'una carpeta local de correu del KMail
+Comment[ca@valencia]=Carrega les dades des d'una carpeta local de correu del KMail
+Comment[cs]=Načítá data z místní složky pošty KMail
+Comment[da]=Indlæser data fra en lokal KMail mail-mappe
+Comment[de]=Daten werden aus einem lokalen KMail-Mailordner geladen
+Comment[el]=Φορτώνει δεδομένα από έναν τοπικό φάκελο αλληλογραφίας του KMail
+Comment[en_GB]=Loads data from a local KMail mail folder
+Comment[es]=Carga datos de una carpeta de correo local de KMail
+Comment[et]=Andmete laadimine kohalikust KMaili kirjade kataloogist
+Comment[fi]=Lataa tietoa paikallisesta KMail-postikansiosta
+Comment[fr]=Charge des données depuis un dossier local KMail
+Comment[ga]=Luchtaíonn sé seo sonraí ó fhillteán logánta KMail
+Comment[gl]=Carga datos desde un cartafol de correo local de KMail.
+Comment[hu]=Adatok betöltése egy helyi KMail levélmappából
+Comment[ia]=Carga datos ex un dossier local de posta de KMail
+Comment[it]=Carica dati da una cartella locale di posta di KMail
+Comment[kk]=Жергілікті KMail пошта қапшығынан деректі алып береді
+Comment[km]=ផ្ទុក​ទិន្នន័យ​ពី​ថត​សំបុត្រ KMail មូលដ្ឋាន
+Comment[ko]=로컬 KMail 메일 폴더에서 데이터를 가져옵니다
+Comment[lt]=Įkelia duomenis iš vietinio KMail aplanko
+Comment[lv]=Ielādē datus no lokālās KMail pasta mapes
+Comment[nb]=Laster data fra en lokal KMail e-ppstmappe
+Comment[nds]=Laadt Daten ut en lokaal KMail-Nettpostorner
+Comment[nl]=Laadt gegevens van een lokale e-mailmap van Kmail
+Comment[pl]=Wczytuje dane z lokalnego katalogu poczty KMail
+Comment[pt]=Carrega os dados a partir de uma pasta de correio local do KMail
+Comment[pt_BR]=Carrega os dados de uma pasta de e-mails local do KMail
+Comment[ru]=Загрузка данных из локальной почтовой папки KMail
+Comment[sk]=Načíta dáta z miestneho mailového priečinka KMail
+Comment[sl]=Naloži podatke iz krajevne poštne mape za KMail
+Comment[sr]=Учитава податке из локалне К‑поштине поштанске фасцикле
+Comment[sr@ijekavian]=Учитава податке из локалне К‑поштине поштанске фасцикле
+Comment[sr@ijekavianlatin]=Učitava podatke iz lokalne K‑poštine poštanske fascikle
+Comment[sr@latin]=Učitava podatke iz lokalne K‑poštine poštanske fascikle
+Comment[sv]=Laddar data från en lokal Kmail brevkatalog
+Comment[tr]=Yerel KMail maildir dizininden veri yükleme aracı
+Comment[uk]=Завантажує дані з локальної теки пошти KMail
+Comment[x-test]=xxLoads data from a local KMail mail folderxx
+Comment[zh_CN]=从本地 KMail 邮件文件夹中载入数据
+Comment[zh_TW]=從本地 KMail 資料夾中載入資料
+Type=AkonadiResource
+Exec=akonadi_mixedmaildir_resource
+Icon=message-rfc822
+
+X-Akonadi-MimeTypes=message/rfc822
+X-Akonadi-Capabilities=Resource
+X-Akonadi-Identifier=akonadi_mixedmaildir_resource
+X-Akonadi-Custom-HasLocalStorage=true
diff --git a/resources/mixedmaildir/mixedmaildirresource.h b/resources/mixedmaildir/mixedmaildirresource.h
new file mode 100644 (file)
index 0000000..03fa499
--- /dev/null
@@ -0,0 +1,113 @@
+/*  This file is part of the KDE project
+    Copyright (c) 2007 Till Adam <adam@kde.org>
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef MIXEDMAILDIR_RESOURCE_H
+#define MIXEDMAILDIR_RESOURCE_H
+
+#include <resourcebase.h>
+
+#include <QStringList>
+
+class CompactChangeHelper;
+class MixedMaildirStore;
+
+class MixedMaildirResource : public Akonadi::ResourceBase, public Akonadi::AgentBase::ObserverV2
+{
+    Q_OBJECT
+
+public:
+    explicit MixedMaildirResource(const QString &id);
+    ~MixedMaildirResource();
+
+public Q_SLOTS:
+    void configure(WId windowId) Q_DECL_OVERRIDE;
+
+protected Q_SLOTS:
+    void retrieveCollections() Q_DECL_OVERRIDE;
+    void retrieveItems(const Akonadi::Collection &col) Q_DECL_OVERRIDE;
+    bool retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+
+protected:
+    void aboutToQuit() Q_DECL_OVERRIDE;
+
+    void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    void itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+    void itemMoved(const Akonadi::Item &item, const Akonadi::Collection &source, const Akonadi::Collection &dest) Q_DECL_OVERRIDE;
+    void itemRemoved(const Akonadi::Item &item) Q_DECL_OVERRIDE;
+
+    void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) Q_DECL_OVERRIDE;
+    void collectionChanged(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    void collectionChanged(const Akonadi::Collection &collection, const QSet<QByteArray> &changedAttributes) Q_DECL_OVERRIDE;
+    void collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &source, const Akonadi::Collection &dest) Q_DECL_OVERRIDE;
+    void collectionRemoved(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+
+private:
+    bool ensureDirExists();
+    bool ensureSaneConfiguration();
+
+    void checkForInvalidatedIndexCollections(KJob *job);
+
+private Q_SLOTS:
+    void reapplyConfiguration();
+
+    void retrieveCollectionsResult(KJob *job);
+    void retrieveItemsResult(KJob *job);
+    void retrieveItemResult(KJob *job);
+
+    void itemAddedResult(KJob *job);
+    void itemChangedResult(KJob *job);
+    void itemMovedResult(KJob *job);
+    void itemRemovedResult(KJob *job);
+
+    void itemsDeleted(KJob *job);
+
+    void collectionAddedResult(KJob *job);
+    void collectionChangedResult(KJob *job);
+    void collectionMovedResult(KJob *job);
+    void collectionRemovedResult(KJob *job);
+
+    void compactStore(const QVariant &arg);
+    void compactStoreResult(KJob *job);
+
+    void restoreTags(const QVariant &arg);
+    void processNextTagContext();
+    void tagFetchJobResult(KJob *job);
+
+private:
+    MixedMaildirStore *mStore;
+
+    struct TagContext {
+        Akonadi::Item mItem;
+        QStringList mTagList;
+    };
+
+    typedef QList<TagContext> TagContextList;
+    QHash<Akonadi::Collection::Id, TagContextList> mTagContextByColId;
+    TagContextList mPendingTagContexts;
+
+    QSet<Akonadi::Collection::Id> mSynchronizedCollections;
+    QSet<Akonadi::Collection::Id> mPendingSynchronizeCollections;
+
+    CompactChangeHelper *mCompactHelper;
+};
+
+#endif
+
diff --git a/resources/mixedmaildir/mixedmaildirresource.kcfg b/resources/mixedmaildir/mixedmaildirresource.kcfg
new file mode 100644 (file)
index 0000000..5c993cf
--- /dev/null
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:kcfg="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
+      http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
+  <kcfgfile/>
+  <group name="General">
+    <entry name="Path" type="Path">
+      <label>Path to KMail mail folder</label>
+      <default></default>
+    </entry>
+    <entry name="TopLevelIsContainer" type="Bool">
+      <label>Path points to a folder containing Maildirs instead of to a maildir itself.</label>
+      <default>false</default>
+    </entry>
+    <entry name="ReadOnly" type="Bool">
+      <label>Do not change the actual backend data.</label>
+      <default>false</default>
+    </entry>
+  </group>
+</kcfg>
diff --git a/resources/mixedmaildir/mixedmaildirresource_debug.cpp b/resources/mixedmaildir/mixedmaildirresource_debug.cpp
new file mode 100644 (file)
index 0000000..d3d0e4b
--- /dev/null
@@ -0,0 +1,22 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2014 Laurent Montel <montel@kde.org>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "mixedmaildirresource_debug.h"
+Q_LOGGING_CATEGORY(MIXEDMAILDIRRESOURCE_LOG, "log_mixedmaildirresource")
+
diff --git a/resources/mixedmaildir/mixedmaildirresource_debug.h b/resources/mixedmaildir/mixedmaildirresource_debug.h
new file mode 100644 (file)
index 0000000..3dd72ce
--- /dev/null
@@ -0,0 +1,27 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2014 Laurent Montel <montel@kde.org>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef MIXEDMAILDIRRESOURCE_DEBUG_H
+#define MIXEDMAILDIRRESOURCE_DEBUG_H
+
+#include <QLoggingCategory>
+Q_DECLARE_LOGGING_CATEGORY(MIXEDMAILDIRRESOURCE_LOG)
+
+#endif
+
diff --git a/resources/mixedmaildir/mixedmaildirstore.cpp b/resources/mixedmaildir/mixedmaildirstore.cpp
new file mode 100644 (file)
index 0000000..137a7cd
--- /dev/null
@@ -0,0 +1,2390 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "mixedmaildirstore.h"
+
+#include "kmindexreader/kmindexreader.h"
+#include "mixedmaildir_debug.h"
+
+#include "filestore/collectioncreatejob.h"
+#include "filestore/collectiondeletejob.h"
+#include "filestore/collectionfetchjob.h"
+#include "filestore/collectionmovejob.h"
+#include "filestore/collectionmodifyjob.h"
+#include "filestore/entitycompactchangeattribute.h"
+#include "filestore/itemcreatejob.h"
+#include "filestore/itemdeletejob.h"
+#include "filestore/itemfetchjob.h"
+#include "filestore/itemmodifyjob.h"
+#include "filestore/itemmovejob.h"
+#include "filestore/storecompactjob.h"
+
+#include "libmaildir/maildir.h"
+
+#include <kmbox/mbox.h>
+#include <kmime/kmime_message.h>
+
+#include <Akonadi/KMime/MessageParts>
+#include <Akonadi/KMime/MessageFlags>
+#include <AkonadiCore/cachepolicy.h>
+#include <AkonadiCore/itemfetchscope.h>
+
+#include <KLocalizedString>
+
+#include "mixedmaildirresource_debug.h"
+
+#include <QDir>
+#include <QFileInfo>
+#include <QStringList>
+
+using namespace Akonadi;
+using KPIM::Maildir;
+using namespace KMBox;
+
+static bool fullEntryCompare(const KMBox::MBoxEntry &left, const KMBox::MBoxEntry &right)
+{
+    return (left.messageOffset() == right.messageOffset() &&
+            left.separatorSize() == right.separatorSize() &&
+            left.messageSize() == right.messageSize());
+}
+
+static bool indexFileForFolder(const QFileInfo &folderDirInfo, QFileInfo &indexFileInfo)
+{
+    indexFileInfo = QFileInfo(folderDirInfo.dir(), QStringLiteral(".%1.index").arg(folderDirInfo.fileName()));
+
+    if (!indexFileInfo.exists() || !indexFileInfo.isReadable()) {
+        qCDebug(MIXEDMAILDIR_LOG) << "No index file" << indexFileInfo.absoluteFilePath() << "or not readable";
+        return false;
+    }
+
+    return true;
+}
+
+class MBoxContext
+{
+public:
+    MBoxContext() : mRevision(0), mIndexDataLoaded(false), mHasIndexData(false) {}
+
+    QString fileName() const
+    {
+        return mMBox.fileName();
+    }
+
+    bool load(const QString &fileName)
+    {
+        mModificationTime = QFileInfo(fileName).lastModified();
+
+        // in case of reload, check if anything changed, otherwise keep deleted entries
+        if (!mDeletedOffsets.isEmpty() && fileName == mMBox.fileName()) {
+            const KMBox::MBoxEntry::List currentEntryList = mMBox.entries();
+            if (mMBox.load(fileName)) {
+                const KMBox::MBoxEntry::List newEntryList = mMBox.entries();
+                if (!std::equal(currentEntryList.begin(), currentEntryList.end(), newEntryList.begin(), fullEntryCompare)) {
+                    mDeletedOffsets.clear();
+                }
+                return true;
+            }
+
+            return false;
+        }
+
+        mDeletedOffsets.clear();
+        return mMBox.load(fileName);
+    }
+
+    QDateTime modificationTime() const
+    {
+        return mModificationTime;
+    }
+
+    KMBox::MBoxEntry::List entryList() const
+    {
+        KMBox::MBoxEntry::List result;
+        Q_FOREACH (const KMBox::MBoxEntry &entry, mMBox.entries()) {
+            if (!mDeletedOffsets.contains(entry.messageOffset())) {
+                result << entry;
+            }
+        }
+        return result;
+    }
+
+    QByteArray readRawEntry(quint64 offset)
+    {
+        return mMBox.readRawMessage(KMBox::MBoxEntry(offset));
+    }
+
+    QByteArray readEntryHeaders(quint64 offset)
+    {
+        return mMBox.readMessageHeaders(KMBox::MBoxEntry(offset));
+    }
+
+    qint64 appendEntry(const KMime::Message::Ptr &entry)
+    {
+        const KMBox::MBoxEntry result = mMBox.appendMessage(entry);
+        if (result.isValid() && mHasIndexData) {
+            mIndexData.insert(result.messageOffset(), KMIndexDataPtr(new KMIndexData));
+            Q_ASSERT(mIndexData.value(result.messageOffset())->isEmpty());
+        }
+
+        return result.messageOffset();
+    }
+
+    void deleteEntry(quint64 offset)
+    {
+        mDeletedOffsets << offset;
+    }
+
+    bool isValidOffset(quint64 offset) const
+    {
+        if (mDeletedOffsets.contains(offset)) {
+            return false;
+        }
+
+        Q_FOREACH (const KMBox::MBoxEntry &entry, mMBox.entries()) {
+            if (entry.messageOffset() == offset) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    bool save()
+    {
+        bool ret = mMBox.save();
+        mModificationTime = QDateTime::currentDateTime();
+        return ret;
+    }
+
+    int purge(QList<KMBox::MBoxEntry::Pair> &movedEntries)
+    {
+        const int deleteCount = mDeletedOffsets.count();
+
+        KMBox::MBoxEntry::List deletedEntries;
+        deletedEntries.reserve(deleteCount);
+        Q_FOREACH (quint64 offset, mDeletedOffsets) {
+            deletedEntries << KMBox::MBoxEntry(offset);
+        }
+
+        const bool result = mMBox.purge(deletedEntries, &movedEntries);
+
+        if (mHasIndexData) {
+            // keep copy of original for lookup
+            const IndexDataHash indexData = mIndexData;
+
+            // delete index data for removed entries
+            Q_FOREACH (quint64 offset, mDeletedOffsets) {
+                mIndexData.remove(offset);
+            }
+
+            // delete index data for changed entries
+            // readded below in an extra loop to handled cases where a new index is equal to an
+            // old index of a different entry
+            Q_FOREACH (const KMBox::MBoxEntry::Pair &entry, movedEntries) {
+                mIndexData.remove(entry.first.messageOffset());
+            }
+
+            // readd index data for changed entries at their new position
+            Q_FOREACH (const KMBox::MBoxEntry::Pair &entry, movedEntries) {
+                const KMIndexDataPtr data = indexData.value(entry.first.messageOffset());
+                mIndexData.insert(entry.second.messageOffset(), data);
+            }
+        }
+
+        mDeletedOffsets.clear();
+        mModificationTime = QDateTime::currentDateTime();
+        return (result ? deleteCount : -1);
+    }
+
+    MBox &mbox()
+    {
+        return mMBox;
+    }
+
+    const MBox &mbox() const
+    {
+        return mMBox;
+    }
+
+    bool hasDeletedOffsets() const
+    {
+        return !mDeletedOffsets.isEmpty();
+    }
+
+    void readIndexData();
+
+    KMIndexDataPtr indexData(quint64 offset) const
+    {
+        if (mHasIndexData) {
+            if (!mDeletedOffsets.contains(offset)) {
+                IndexDataHash::const_iterator it = mIndexData.constFind(offset);
+                if (it != mIndexData.constEnd()) {
+                    return it.value();
+                }
+            }
+        }
+
+        return KMIndexDataPtr();
+    }
+
+    bool hasIndexData() const
+    {
+        return mHasIndexData;
+    }
+
+    void updatePath(const QString &newPath)
+    {
+        // TODO FIXME there has to be a better of doing that
+        mMBox.load(newPath);
+    }
+
+public:
+    Collection mCollection;
+    qint64 mRevision;
+
+private:
+    QSet<quint64> mDeletedOffsets;
+    MBox mMBox;
+    QDateTime mModificationTime;
+
+    typedef QHash<quint64, KMIndexDataPtr> IndexDataHash;
+    IndexDataHash mIndexData;
+    bool mIndexDataLoaded;
+    bool mHasIndexData;
+};
+
+typedef QSharedPointer<MBoxContext> MBoxPtr;
+
+void MBoxContext::readIndexData()
+{
+    if (mIndexDataLoaded) {
+        return;
+    }
+
+    mIndexDataLoaded = true;
+
+    const QFileInfo mboxFileInfo(mMBox.fileName());
+    QFileInfo indexFileInfo;
+    if (!indexFileForFolder(mboxFileInfo, indexFileInfo)) {
+        return;
+    }
+
+    if (mboxFileInfo.lastModified() > indexFileInfo.lastModified()) {
+        qCDebug(MIXEDMAILDIR_LOG) << "MBox file " << mboxFileInfo.absoluteFilePath()
+                                  << "newer than the index: mbox modified at"
+                                  << mboxFileInfo.lastModified() << ", index modified at"
+                                  << indexFileInfo.lastModified();
+        return;
+    }
+
+    KMIndexReader indexReader(indexFileInfo.absoluteFilePath());
+    if (indexReader.error() || !indexReader.readIndex()) {
+        qCritical() << "Index file" << indexFileInfo.path() << "could not be read";
+        return;
+    }
+
+    mHasIndexData = true;
+
+    const KMBox::MBoxEntry::List entries = mMBox.entries();
+    Q_FOREACH (const KMBox::MBoxEntry &entry, entries) {
+        const quint64 indexOffset = entry.messageOffset() + entry.separatorSize();
+        const KMIndexDataPtr data = indexReader.dataByOffset(indexOffset);
+        if (data != Q_NULLPTR) {
+            mIndexData.insert(entry.messageOffset(), data);
+        }
+    }
+
+    qCDebug(MIXEDMAILDIR_LOG) << "Read" << mIndexData.count() << "index entries from"
+                              << indexFileInfo.absoluteFilePath();
+}
+
+class MaildirContext
+{
+public:
+    MaildirContext(const QString &path, bool isTopLevel)
+        : mMaildir(path, isTopLevel), mIndexDataLoaded(false), mHasIndexData(false)
+    {
+    }
+
+    MaildirContext(const Maildir &md)
+        : mMaildir(md), mIndexDataLoaded(false), mHasIndexData(false)
+    {
+    }
+
+    QStringList entryList() const
+    {
+        return mMaildir.entryList();
+    }
+
+    QString addEntry(const QByteArray &data)
+    {
+        const QString result = mMaildir.addEntry(data);
+        if (!result.isEmpty() && mHasIndexData) {
+            mIndexData.insert(result, KMIndexDataPtr(new KMIndexData));
+            Q_ASSERT(mIndexData.value(result)->isEmpty());
+        } else {
+            //TODO: use the error string?
+            qCWarning(MIXEDMAILDIRRESOURCE_LOG) << mMaildir.lastError();
+        }
+
+        return result;
+    }
+
+    void writeEntry(const QString &key, const QByteArray &data)
+    {
+        mMaildir.writeEntry(key, data);   //TODO: error handling
+        if (mHasIndexData) {
+            mIndexData.insert(key, KMIndexDataPtr(new KMIndexData));
+        }
+    }
+
+    bool removeEntry(const QString &key)
+    {
+        const bool result = mMaildir.removeEntry(key);
+        if (result && mHasIndexData) {
+            mIndexData.remove(key);
+        }
+
+        return result;
+    }
+
+    QString moveEntryTo(const QString &key, MaildirContext &destination)
+    {
+        const QString result = mMaildir.moveEntryTo(key, destination.mMaildir);
+        if (!result.isEmpty()) {
+            if (mHasIndexData) {
+                mIndexData.remove(key);
+            }
+
+            if (destination.mHasIndexData) {
+                destination.mIndexData.insert(result, KMIndexDataPtr(new KMIndexData));
+            }
+        } else {
+            //TODO error handling?
+            qCWarning(MIXEDMAILDIRRESOURCE_LOG) << mMaildir.lastError();
+        }
+
+        return result;
+    }
+
+    QByteArray readEntryHeaders(const QString &key) const
+    {
+        return mMaildir.readEntryHeaders(key);
+    }
+
+    QByteArray readEntry(const QString &key) const
+    {
+        return mMaildir.readEntry(key);
+    }
+
+    bool isValid(QString &error) const
+    {
+        bool result = mMaildir.isValid();
+        if (!result) {
+            error = mMaildir.lastError();
+        }
+        return result;
+    }
+
+    bool isValidEntry(const QString &entry) const
+    {
+        return !mMaildir.findRealKey(entry).isEmpty();
+    }
+
+    void readIndexData();
+
+    KMIndexDataPtr indexData(const QString &fileName) const
+    {
+        if (mHasIndexData) {
+            IndexDataHash::const_iterator it = mIndexData.constFind(fileName);
+            if (it != mIndexData.constEnd()) {
+                return it.value();
+            }
+        }
+
+        return KMIndexDataPtr();
+    }
+
+    bool hasIndexData() const
+    {
+        return mHasIndexData;
+    }
+
+    void updatePath(const QString &newPath)
+    {
+        mMaildir = Maildir(newPath, mMaildir.isRoot());
+    }
+
+    const Maildir &maildir() const
+    {
+        return mMaildir;
+    }
+
+private:
+    Maildir mMaildir;
+
+    typedef QHash<QString, KMIndexDataPtr> IndexDataHash;
+    IndexDataHash mIndexData;
+    bool mIndexDataLoaded;
+    bool mHasIndexData;
+};
+
+void MaildirContext::readIndexData()
+{
+    if (mIndexDataLoaded) {
+        return;
+    }
+
+    mIndexDataLoaded = true;
+
+    const QFileInfo maildirFileInfo(mMaildir.path());
+    QFileInfo indexFileInfo;
+    if (!indexFileForFolder(maildirFileInfo, indexFileInfo)) {
+        return;
+    }
+
+    const QDir maildirBaseDir(maildirFileInfo.absoluteFilePath());
+    const QFileInfo curDirFileInfo(maildirBaseDir, QStringLiteral("cur"));
+    const QFileInfo newDirFileInfo(maildirBaseDir, QStringLiteral("new"));
+
+    if (curDirFileInfo.lastModified() > indexFileInfo.lastModified()) {
+        qCDebug(MIXEDMAILDIR_LOG) << "Maildir " << maildirFileInfo.absoluteFilePath()
+                                  << "\"cur\" directory newer than the index: cur modified at"
+                                  << curDirFileInfo.lastModified() << ", index modified at"
+                                  << indexFileInfo.lastModified();
+        return;
+    }
+    if (newDirFileInfo.lastModified() > indexFileInfo.lastModified()) {
+        qCDebug(MIXEDMAILDIR_LOG) << "Maildir \"new\" directory newer than the index: cur modified at"
+                                  << newDirFileInfo.lastModified() << ", index modified at"
+                                  << indexFileInfo.lastModified();
+        return;
+    }
+
+    KMIndexReader indexReader(indexFileInfo.absoluteFilePath());
+    if (indexReader.error() || !indexReader.readIndex()) {
+        qCritical() << "Index file" << indexFileInfo.path() << "could not be read";
+        return;
+    }
+
+    mHasIndexData = true;
+
+    const QStringList entries = mMaildir.entryList();
+    Q_FOREACH (const QString &entry, entries) {
+        const KMIndexDataPtr data = indexReader.dataByFileName(entry);
+        if (data != Q_NULLPTR) {
+            mIndexData.insert(entry, data);
+        }
+    }
+
+    qCDebug(MIXEDMAILDIR_LOG) << "Read" << mIndexData.count() << "index entries from"
+                              << indexFileInfo.absoluteFilePath();
+}
+
+typedef QSharedPointer<MaildirContext> MaildirPtr;
+
+class MixedMaildirStore::Private : public FileStore::Job::Visitor
+{
+    MixedMaildirStore *const q;
+
+public:
+    enum FolderType {
+        InvalidFolder,
+        TopLevelFolder,
+        MaildirFolder,
+        MBoxFolder
+    };
+
+    Private(MixedMaildirStore *parent) : q(parent)
+    {
+    }
+
+    FolderType folderForCollection(const Collection &col, QString &path, QString &errorText) const;
+
+    MBoxPtr getOrCreateMBoxPtr(const QString &path);
+    MaildirPtr getOrCreateMaildirPtr(const QString &path, bool isTopLevel);
+
+    void fillMBoxCollectionDetails(const MBoxPtr &mbox, Collection &collection);
+    void fillMaildirCollectionDetails(const Maildir &md, Collection &collection);
+    void fillMaildirTreeDetails(const Maildir &md, const Collection &collection, Collection::List &collections, bool recurse);
+    void listCollection(FileStore::Job *job, MBoxPtr &mbox, const Collection &collection, Item::List &items);
+    void listCollection(FileStore::Job *job, MaildirPtr &md, const Collection &collection, Item::List &items);
+    bool fillItem(MBoxPtr &mbox, bool includeHeaders, bool includeBody, Item &item) const;
+    bool fillItem(const MaildirPtr &md, bool includeHeaders, bool includeBody, Item &item) const;
+    void updateContextHashes(const QString &oldPath, const QString &newPath);
+
+public: // visitor interface implementation
+    bool visit(FileStore::Job *job) Q_DECL_OVERRIDE;
+    bool visit(FileStore::CollectionCreateJob *job) Q_DECL_OVERRIDE;
+    bool visit(FileStore::CollectionDeleteJob *job) Q_DECL_OVERRIDE;
+    bool visit(FileStore::CollectionFetchJob *job) Q_DECL_OVERRIDE;
+    bool visit(FileStore::CollectionModifyJob *job) Q_DECL_OVERRIDE;
+    bool visit(FileStore::CollectionMoveJob *job) Q_DECL_OVERRIDE;
+    bool visit(FileStore::ItemCreateJob *job) Q_DECL_OVERRIDE;
+    bool visit(FileStore::ItemDeleteJob *job) Q_DECL_OVERRIDE;
+    bool visit(FileStore::ItemFetchJob *job) Q_DECL_OVERRIDE;
+    bool visit(FileStore::ItemModifyJob *job) Q_DECL_OVERRIDE;
+    bool visit(FileStore::ItemMoveJob *job) Q_DECL_OVERRIDE;
+    bool visit(FileStore::StoreCompactJob *job) Q_DECL_OVERRIDE;
+
+public:
+    typedef QHash<QString, MBoxPtr> MBoxHash;
+    MBoxHash mMBoxes;
+
+    typedef QHash<QString, MaildirPtr> MaildirHash;
+    MaildirHash mMaildirs;
+};
+
+MixedMaildirStore::Private::FolderType MixedMaildirStore::Private::folderForCollection(const Collection &col, QString &path, QString &errorText) const
+{
+    path.clear();
+    errorText.clear();
+
+    if (col.remoteId().isEmpty()) {
+        errorText = i18nc("@info:status", "Given folder name is empty");
+        qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "Incomplete ancestor chain for collection.";
+        Q_ASSERT(!col.remoteId().isEmpty());   // abort! Look at backtrace to see where we came from.
+        return InvalidFolder;
+    }
+
+    if (col.parentCollection() == Collection::root()) {
+        path = q->path();
+        if (col.remoteId() != path) {
+            qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "RID mismatch, is" << col.remoteId() << "expected" << path;
+        }
+        return TopLevelFolder;
+    }
+
+    FolderType type = folderForCollection(col.parentCollection(), path, errorText);
+    switch (type) {
+    case InvalidFolder: return InvalidFolder;
+
+    case TopLevelFolder: // fall through
+    case MaildirFolder: {
+        const Maildir parentMd(path, type == TopLevelFolder);
+        const Maildir subFolder = parentMd.subFolder(col.remoteId());
+        if (subFolder.isValid(false)) {
+            path = subFolder.path();
+            return MaildirFolder;
+        }
+
+        const QString subDirPath =
+            (type == TopLevelFolder ? path : Maildir::subDirPathForFolderPath(path));
+        QFileInfo fileInfo(QDir(subDirPath), col.remoteId());
+        if (fileInfo.isFile()) {
+            path = fileInfo.absoluteFilePath();
+            return MBoxFolder;
+        }
+
+        errorText = i18nc("@info:status", "Folder %1 does not seem to be a valid email folder", fileInfo.absoluteFilePath());
+        return InvalidFolder;
+    }
+
+    case MBoxFolder: {
+        const QString subDirPath = Maildir::subDirPathForFolderPath(path);
+        QFileInfo fileInfo(QDir(subDirPath), col.remoteId());
+
+        if (fileInfo.isFile()) {
+            path = fileInfo.absoluteFilePath();
+            return MBoxFolder;
+        }
+
+        const Maildir subFolder(fileInfo.absoluteFilePath(), false);
+        if (subFolder.isValid(false)) {
+            path = subFolder.path();
+            return MaildirFolder;
+        }
+
+        errorText = i18nc("@info:status", "Folder %1 does not seem to be a valid email folder", fileInfo.absoluteFilePath());
+        return InvalidFolder;
+    }
+    }
+    return InvalidFolder;
+}
+
+MBoxPtr MixedMaildirStore::Private::getOrCreateMBoxPtr(const QString &path)
+{
+    MBoxPtr mbox;
+    const MBoxHash::const_iterator it = mMBoxes.constFind(path);
+    if (it == mMBoxes.constEnd()) {
+        mbox = MBoxPtr(new MBoxContext);
+        mMBoxes.insert(path, mbox);
+    } else {
+        mbox = it.value();
+    }
+
+    return mbox;
+}
+
+MaildirPtr MixedMaildirStore::Private::getOrCreateMaildirPtr(const QString &path, bool isTopLevel)
+{
+    MaildirPtr md;
+    const MaildirHash::const_iterator it = mMaildirs.constFind(path);
+    if (it == mMaildirs.constEnd()) {
+        md = MaildirPtr(new MaildirContext(path, isTopLevel));
+        mMaildirs.insert(path, md);
+    } else {
+        md = it.value();
+    }
+
+    return md;
+}
+
+void MixedMaildirStore::Private::fillMBoxCollectionDetails(const MBoxPtr &mbox, Collection &collection)
+{
+    collection.setContentMimeTypes(QStringList() << Collection::mimeType()
+                                   << KMime::Message::mimeType());
+
+    const QFileInfo fileInfo(mbox->fileName());
+    if (fileInfo.isWritable()) {
+        collection.setRights(Collection::CanCreateItem |
+                             Collection::CanChangeItem |
+                             Collection::CanDeleteItem |
+                             Collection::CanCreateCollection |
+                             Collection::CanChangeCollection |
+                             Collection::CanDeleteCollection);
+    } else {
+        collection.setRights(Collection::ReadOnly);
+    }
+
+    if (mbox->mRevision > 0) {
+        collection.setRemoteRevision(QString::number(mbox->mRevision));
+    }
+}
+
+void MixedMaildirStore::Private::fillMaildirCollectionDetails(const Maildir &md, Collection &collection)
+{
+    collection.setContentMimeTypes(QStringList() << Collection::mimeType()
+                                   << KMime::Message::mimeType());
+
+    const QFileInfo fileInfo(md.path());
+    if (fileInfo.isWritable()) {
+        collection.setRights(Collection::CanCreateItem |
+                             Collection::CanChangeItem |
+                             Collection::CanDeleteItem |
+                             Collection::CanCreateCollection |
+                             Collection::CanChangeCollection |
+                             Collection::CanDeleteCollection);
+    } else {
+        collection.setRights(Collection::ReadOnly);
+    }
+}
+
+void MixedMaildirStore::Private::fillMaildirTreeDetails(const Maildir &md, const Collection &collection, Collection::List &collections, bool recurse)
+{
+    if (md.path().isEmpty()) {
+        return;
+    }
+
+    const QStringList maildirSubFolders = md.subFolderList();
+    Q_FOREACH (const QString &subFolder, maildirSubFolders) {
+        const Maildir subMd = md.subFolder(subFolder);
+
+        if (!mMaildirs.contains(subMd.path())) {
+            const MaildirPtr mdPtr = MaildirPtr(new MaildirContext(subMd));
+            mMaildirs.insert(subMd.path(), mdPtr);
+        }
+
+        Collection col;
+        col.setRemoteId(subFolder);
+        col.setName(subFolder);
+        col.setParentCollection(collection);
+        fillMaildirCollectionDetails(subMd, col);
+        collections << col;
+
+        if (recurse) {
+            fillMaildirTreeDetails(subMd, col, collections, true);
+        }
+    }
+
+    const QDir dir(md.isRoot() ? md.path() : Maildir::subDirPathForFolderPath(md.path()));
+    const QFileInfoList fileInfos = dir.entryInfoList(QDir::Files);
+    Q_FOREACH (const QFileInfo &fileInfo, fileInfos) {
+        if (fileInfo.isHidden() || !fileInfo.isReadable()) {
+            continue;
+        }
+
+        const QString mboxPath = fileInfo.absoluteFilePath();
+
+        MBoxPtr mbox = getOrCreateMBoxPtr(mboxPath);
+        if (mbox->load(mboxPath)) {
+            const QString subFolder = fileInfo.fileName();
+            Collection col;
+            col.setRemoteId(subFolder);
+            col.setName(subFolder);
+            col.setParentCollection(collection);
+            mbox->mCollection = col;
+
+            fillMBoxCollectionDetails(mbox, col);
+            collections << col;
+
+            if (recurse) {
+                const QString subDirPath = Maildir::subDirPathForFolderPath(fileInfo.absoluteFilePath());
+                const Maildir subMd(subDirPath, true);
+                fillMaildirTreeDetails(subMd, col, collections, true);
+            }
+        } else {
+            mMBoxes.remove(fileInfo.absoluteFilePath());
+        }
+    }
+}
+
+void MixedMaildirStore::Private::listCollection(FileStore::Job *job, MBoxPtr &mbox, const Collection &collection, Item::List &items)
+{
+    mbox->readIndexData();
+
+    QHash<QString, QVariant> uidHash;
+    QHash<QString, QVariant> tagListHash;
+
+    const KMBox::MBoxEntry::List entryList = mbox->entryList();
+    Q_FOREACH (const KMBox::MBoxEntry &entry, entryList) {
+        Item item;
+        item.setMimeType(KMime::Message::mimeType());
+        item.setRemoteId(QString::number(entry.messageOffset()));
+        item.setParentCollection(collection);
+
+        if (mbox->hasIndexData()) {
+            const KMIndexDataPtr indexData = mbox->indexData(entry.messageOffset());
+            if (indexData != Q_NULLPTR && !indexData->isEmpty()) {
+                item.setFlags(indexData->status().statusFlags());
+
+                quint64 uid = indexData->uid();
+                if (uid != 0) {
+                    qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "item" << item.remoteId() << "has UID" << uid;
+                    uidHash.insert(item.remoteId(), QString::number(uid));
+                }
+
+                const QStringList tagList = indexData->tagList();
+                if (!tagList.isEmpty()) {
+                    qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "item" << item.remoteId() << "has"
+                                                      << tagList.count() << "tags:" << tagList;
+                    tagListHash.insert(item.remoteId(), tagList);
+                }
+            } else if (indexData == Q_NULLPTR) {
+                Akonadi::MessageStatus status;
+                status.setDeleted(true),
+                                  item.setFlags(status.statusFlags());
+                qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "no index for item" << item.remoteId() << "in MBox" << mbox->fileName()
+                                                  << "so it has been deleted but not purged. Marking it as"
+                                                  << item.flags();
+            }
+        }
+
+        items << item;
+    }
+
+    if (mbox->hasIndexData()) {
+        QVariant var;
+
+        if (!uidHash.isEmpty()) {
+            var = QVariant::fromValue< QHash<QString, QVariant> >(uidHash);
+            job->setProperty("remoteIdToIndexUid", var);
+        }
+
+        if (!tagListHash.isEmpty()) {
+            var = QVariant::fromValue< QHash<QString, QVariant> >(tagListHash);
+            job->setProperty("remoteIdToTagList", var);
+        }
+    }
+}
+
+void MixedMaildirStore::Private::listCollection(FileStore::Job *job, MaildirPtr &md, const Collection &collection, Item::List &items)
+{
+    md->readIndexData();
+
+    QHash<QString, QVariant> uidHash;
+    QHash<QString, QVariant> tagListHash;
+
+    const QStringList entryList = md->entryList();
+    Q_FOREACH (const QString &entry, entryList) {
+        Item item;
+        item.setMimeType(KMime::Message::mimeType());
+        item.setRemoteId(entry);
+        item.setParentCollection(collection);
+
+        if (md->hasIndexData()) {
+            const KMIndexDataPtr indexData = md->indexData(entry);
+            if (indexData != Q_NULLPTR && !indexData->isEmpty()) {
+                item.setFlags(indexData->status().statusFlags());
+
+                const quint64 uid = indexData->uid();
+                if (uid != 0) {
+                    qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "item" << item.remoteId() << "has UID" << uid;
+                    uidHash.insert(item.remoteId(), QString::number(uid));
+                }
+
+                const QStringList tagList = indexData->tagList();
+                if (!tagList.isEmpty()) {
+                    qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "item" << item.remoteId() << "has"
+                                                      << tagList.count() << "tags:" << tagList;
+                    tagListHash.insert(item.remoteId(), tagList);
+                }
+            }
+        }
+        Akonadi::Item::Flags flags = md->maildir().readEntryFlags(entry);
+        Q_FOREACH (const Akonadi::Item::Flag &flag, flags) {
+            item.setFlag(flag);
+        }
+
+        items << item;
+    }
+
+    if (md->hasIndexData()) {
+        QVariant var;
+
+        if (!uidHash.isEmpty()) {
+            var = QVariant::fromValue< QHash<QString, QVariant> >(uidHash);
+            job->setProperty("remoteIdToIndexUid", var);
+        }
+
+        if (!tagListHash.isEmpty()) {
+            var = QVariant::fromValue< QHash<QString, QVariant> >(tagListHash);
+            job->setProperty("remoteIdToTagList", var);
+        }
+    }
+}
+
+bool MixedMaildirStore::Private::fillItem(MBoxPtr &mbox, bool includeHeaders, bool includeBody, Item &item) const
+{
+//  qCDebug(MIXEDMAILDIR_LOG) << "Filling item" << item.remoteId() << "from MBox: includeBody=" << includeBody;
+    bool ok = false;
+    const quint64 offset = item.remoteId().toULongLong(&ok);
+    if (!ok || !mbox->isValidOffset(offset)) {
+        return false;
+    }
+
+    item.setModificationTime(mbox->modificationTime());
+
+    // TODO: size?
+
+    if (includeHeaders || includeBody) {
+        KMime::Message::Ptr messagePtr(new KMime::Message());
+        if (includeBody) {
+            const QByteArray data = mbox->readRawEntry(offset);
+            messagePtr->setContent(KMime::CRLFtoLF(data));
+        } else {
+            const QByteArray data = mbox->readEntryHeaders(offset);
+            messagePtr->setHead(KMime::CRLFtoLF(data));
+        }
+        messagePtr->parse();
+
+        item.setPayload<KMime::Message::Ptr>(messagePtr);
+        Akonadi::MessageFlags::copyMessageFlags(*messagePtr, item);
+    }
+    return true;
+}
+
+bool MixedMaildirStore::Private::fillItem(const MaildirPtr &md, bool includeHeaders, bool includeBody, Item &item) const
+{
+    /*  qCDebug(MIXEDMAILDIR_LOG) << "Filling item" << item.remoteId() << "from Maildir: includeBody=" << includeBody;*/
+
+    const qint64 entrySize = md->maildir().size(item.remoteId());
+    if (entrySize < 0) {
+        return false;
+    }
+
+    item.setSize(entrySize);
+    item.setModificationTime(md->maildir().lastModified(item.remoteId()));
+
+    if (includeHeaders || includeBody) {
+        KMime::Message::Ptr messagePtr(new KMime::Message());
+        if (includeBody) {
+            const QByteArray data = md->readEntry(item.remoteId());
+            if (data.isEmpty()) {
+                return false;
+            }
+
+            messagePtr->setContent(KMime::CRLFtoLF(data));
+        } else {
+            const QByteArray data = md->readEntryHeaders(item.remoteId());
+            if (data.isEmpty()) {
+                return false;
+            }
+
+            messagePtr->setHead(KMime::CRLFtoLF(data));
+        }
+        messagePtr->parse();
+
+        item.setPayload<KMime::Message::Ptr>(messagePtr);
+        Akonadi::MessageFlags::copyMessageFlags(*messagePtr, item);
+    }
+    return true;
+}
+
+void MixedMaildirStore::Private::updateContextHashes(const QString &oldPath, const QString &newPath)
+{
+    //qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "oldPath=" << oldPath << "newPath=" << newPath;
+    const QString oldSubDirPath = Maildir::subDirPathForFolderPath(oldPath);
+    const QString newSubDirPath = Maildir::subDirPathForFolderPath(newPath);
+
+    MBoxHash mboxes;
+    MBoxHash::const_iterator mboxIt    = mMBoxes.constBegin();
+    MBoxHash::const_iterator mboxEndIt = mMBoxes.constEnd();
+    for (; mboxIt != mboxEndIt; ++mboxIt) {
+        QString key = mboxIt.key();
+        MBoxPtr mboxPtr = mboxIt.value();
+
+        if (key == oldPath) {
+            key = newPath;
+        } else if (key.startsWith(oldSubDirPath)) {
+            if (mboxPtr->hasIndexData() || mboxPtr->mRevision > 0) {
+                key.replace(oldSubDirPath, newSubDirPath);
+            } else {
+                // if there is no index data yet, just discard this context
+                key.clear();
+            }
+        }
+
+        if (!key.isEmpty()) {
+            mboxPtr->updatePath(key);
+            mboxes.insert(key, mboxPtr);
+        }
+    }
+    //qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "mbox: old keys=" << mMBoxes.keys() << "new keys" << mboxes.keys();
+    mMBoxes = mboxes;
+
+    MaildirHash maildirs;
+    MaildirHash::const_iterator mdIt    = mMaildirs.constBegin();
+    MaildirHash::const_iterator mdEndIt = mMaildirs.constEnd();
+    for (; mdIt != mdEndIt; ++mdIt) {
+        QString key = mdIt.key();
+        MaildirPtr mdPtr = mdIt.value();
+
+        if (key == oldPath) {
+            key = newPath;
+        } else if (key.startsWith(oldSubDirPath)) {
+            if (mdPtr->hasIndexData()) {
+                key.replace(oldSubDirPath, newSubDirPath);
+            } else {
+                // if there is no index data yet, just discard this context
+                key.clear();
+            }
+        }
+
+        if (!key.isEmpty()) {
+            mdPtr->updatePath(key);
+            maildirs.insert(key, mdPtr);
+        }
+    }
+    //qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "maildir: old keys=" << mMaildirs.keys() << "new keys" << maildirs.keys();
+    mMaildirs = maildirs;
+}
+
+bool MixedMaildirStore::Private::visit(FileStore::Job *job)
+{
+    const QString message = i18nc("@info:status", "Unhandled operation %1", QLatin1String(job->metaObject()->className()));
+    qCritical() << message;
+    q->notifyError(FileStore::Job::InvalidJobContext, message);
+    return false;
+}
+
+bool MixedMaildirStore::Private::visit(FileStore::CollectionCreateJob *job)
+{
+    QString path;
+    QString errorText;
+
+    const FolderType folderType = folderForCollection(job->targetParent(), path, errorText);
+    if (folderType == InvalidFolder) {
+        errorText = i18nc("@info:status", "Cannot create folder %1 inside folder %2",
+                          job->collection().name(), job->targetParent().name());
+        qCritical() << errorText << "FolderType=" << folderType;
+        q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+        return false;
+    }
+
+    const QString collectionName = job->collection().name().replace(QDir::separator(), QString());
+    Maildir md;
+    if (folderType == MBoxFolder) {
+        const QString subDirPath = Maildir::subDirPathForFolderPath(path);
+        const QDir dir(subDirPath);
+        const QFileInfo dirInfo(dir, collectionName);
+        if (dirInfo.exists() && !dirInfo.isDir()) {
+            errorText = i18nc("@info:status", "Cannot create folder %1 inside folder %2",
+                              job->collection().name(), job->targetParent().name());
+            qCritical() << errorText << "FolderType=" << folderType << ", dirInfo exists and it not a dir";
+            q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+            return false;
+        }
+
+        if (!dir.mkpath(collectionName)) {
+            errorText = i18nc("@info:status", "Cannot create folder %1 inside folder %2",
+                              job->collection().name(), job->targetParent().name());
+            qCritical() << errorText << "FolderType=" << folderType << ", mkpath failed";
+            q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+            return false;
+        }
+
+        md = Maildir(dirInfo.absoluteFilePath(), false);
+        if (!md.create()) {
+            errorText = i18nc("@info:status", "Cannot create folder %1 inside folder %2",
+                              job->collection().name(), job->targetParent().name());
+            qCritical() << errorText << "FolderType=" << folderType << ", maildir create failed";
+            q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+            return false;
+        }
+
+        const MaildirPtr mdPtr(new MaildirContext(md));
+        mMaildirs.insert(md.path(), mdPtr);
+    } else {
+        Maildir parentMd(path, folderType == TopLevelFolder);
+        if (parentMd.addSubFolder(collectionName).isEmpty()) {
+            errorText = i18nc("@info:status", "Cannot create folder %1 inside folder %2",
+                              job->collection().name(), job->targetParent().name());
+            qCritical() << errorText << "FolderType=" << folderType;
+            q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+            return false;
+        }
+
+        md = Maildir(parentMd.subFolder(collectionName));
+        const MaildirPtr mdPtr(new MaildirContext(md));
+        mMaildirs.insert(md.path(), mdPtr);
+    }
+
+    Collection collection = job->collection();
+    collection.setRemoteId(collectionName);
+    collection.setName(collectionName);
+    collection.setParentCollection(job->targetParent());
+    fillMaildirCollectionDetails(md, collection);
+
+    q->notifyCollectionsProcessed(Collection::List() << collection);
+    return true;
+}
+
+bool MixedMaildirStore::Private::visit(FileStore::CollectionDeleteJob *job)
+{
+    QString path;
+    QString errorText;
+
+    const FolderType folderType = folderForCollection(job->collection(), path, errorText);
+    if (folderType == InvalidFolder) {
+        errorText = i18nc("@info:status", "Cannot remove folder %1 from folder %2",
+                          job->collection().name(), job->collection().parentCollection().name());
+        qCritical() << errorText << "FolderType=" << folderType;
+        q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+        return false;
+    }
+
+    QString parentPath;
+    const FolderType parentFolderType = folderForCollection(job->collection().parentCollection(), parentPath, errorText);
+    if (parentFolderType == InvalidFolder) {
+        errorText = i18nc("@info:status", "Cannot remove folder %1 from folder %2",
+                          job->collection().name(), job->collection().parentCollection().name());
+        qCritical() << errorText << "Parent FolderType=" << parentFolderType;
+        q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+        return false;
+    }
+
+    if (folderType == MBoxFolder) {
+        if (!QFile::remove(path)) {
+            errorText = i18nc("@info:status", "Cannot remove folder %1 from folder %2",
+                              job->collection().name(), job->collection().parentCollection().name());
+            qCritical() << errorText << "FolderType=" << folderType;
+            q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+            return false;
+        }
+    } else {
+        if (!QDir(path).removeRecursively()) {
+            errorText = i18nc("@info:status", "Cannot remove folder %1 from folder %2",
+                              job->collection().name(), job->collection().parentCollection().name());
+            qCritical() << errorText << "FolderType=" << folderType;
+            q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+            return false;
+        }
+    }
+
+    const QString subDirPath = Maildir::subDirPathForFolderPath(path);
+    QDir(subDirPath).removeRecursively();
+
+    q->notifyCollectionsProcessed(Collection::List() << job->collection());
+    return true;
+}
+
+bool MixedMaildirStore::Private::visit(FileStore::CollectionFetchJob *job)
+{
+    QString path;
+    QString errorText;
+    const FolderType folderType = folderForCollection(job->collection(), path, errorText);
+
+    if (folderType == InvalidFolder) {
+        qCritical() << errorText << "collection:" << job->collection();
+        q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+        return false;
+    }
+
+    Collection::List collections;
+    Collection collection = job->collection();
+    if (job->type() == FileStore::CollectionFetchJob::Base) {
+        collection.setName(collection.remoteId());
+        if (folderType == MBoxFolder) {
+            MBoxPtr mbox;
+            MBoxHash::const_iterator findIt = mMBoxes.constFind(path);
+            if (findIt == mMBoxes.constEnd()) {
+                mbox = MBoxPtr(new MBoxContext);
+                if (!mbox->load(path)) {
+                    errorText = i18nc("@info:status", "Failed to load MBox folder %1", path);
+                    qCritical() << errorText << "collection=" << collection;
+                    q->notifyError(FileStore::Job::InvalidJobContext, errorText);   // TODO should be a different error code
+                    return false;
+                }
+
+                mbox->mCollection = collection;
+                mMBoxes.insert(path, mbox);
+            } else {
+                mbox = findIt.value();
+            }
+
+            fillMBoxCollectionDetails(mbox, collection);
+        } else {
+            const Maildir md(path, folderType == TopLevelFolder);
+            fillMaildirCollectionDetails(md, collection);
+        }
+        collections << collection;
+    } else {
+        // if the base is an mbox, use its sub folder dir like a top level maildir
+        if (folderType == MBoxFolder) {
+            path = Maildir::subDirPathForFolderPath(path);
+        }
+        const Maildir md(path, folderType != MaildirFolder);
+        fillMaildirTreeDetails(md, collection, collections,
+                               job->type() == FileStore::CollectionFetchJob::Recursive);
+    }
+
+    if (!collections.isEmpty()) {
+        q->notifyCollectionsProcessed(collections);
+    }
+    return true;
+}
+
+static Collection updateMBoxCollectionTree(const Collection &collection, const Collection &oldParent, const Collection &newParent)
+{
+    if (collection == oldParent) {
+        return newParent;
+    }
+
+    if (collection == Collection::root()) {
+        return collection;
+    }
+
+    Collection updatedCollection = collection;
+    updatedCollection.setParentCollection(updateMBoxCollectionTree(collection.parentCollection(), oldParent, newParent));
+
+    return updatedCollection;
+}
+
+bool MixedMaildirStore::Private::visit(FileStore::CollectionModifyJob *job)
+{
+    const Collection collection = job->collection();
+    const QString collectionName = collection.name().replace(QDir::separator(), QString());
+
+    // we also only do renames
+    if (collection.remoteId() == collection.name()) {
+        qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "CollectionModifyJob with name still identical to remoteId. Ignoring";
+        return true;
+    }
+
+    QString path;
+    QString errorText;
+    const FolderType folderType = folderForCollection(collection, path, errorText);
+    if (folderType == InvalidFolder) {
+        errorText = i18nc("@info:status", "Cannot rename folder %1",
+                          collection.name());
+        qCritical() << errorText << "FolderType=" << folderType;
+        q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+        return false;
+    }
+
+    const QFileInfo fileInfo(path);
+    const QFileInfo subDirInfo = Maildir::subDirPathForFolderPath(path);
+
+    QDir parentDir(path);
+    parentDir.cdUp();
+
+    const QFileInfo targetFileInfo(parentDir, collectionName);
+    const QFileInfo targetSubDirInfo = Maildir::subDirPathForFolderPath(targetFileInfo.absoluteFilePath());
+
+    if (targetFileInfo.exists() || (subDirInfo.exists() && targetSubDirInfo.exists())) {
+        errorText = i18nc("@info:status", "Cannot rename folder %1",
+                          collection.name());
+        qCritical() << errorText << "FolderType=" << folderType;
+        q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+        return false;
+    }
+
+    // if there is an index, make sure it is read before renaming
+    // do not rename index as it could already be out of date
+    bool indexInvalidated = false;
+    if (folderType == MBoxFolder) {
+        // TODO would be nice if getOrCreateMBoxPtr() could be used instead, like below for Maildir
+        MBoxPtr mbox;
+        MBoxHash::const_iterator findIt = mMBoxes.constFind(path);
+        if (findIt == mMBoxes.constEnd()) {
+            mbox = MBoxPtr(new MBoxContext);
+            if (!mbox->load(path)) {
+                qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "Failed to load mbox" << path;
+            }
+
+            mbox->mCollection = collection;
+            mMBoxes.insert(path, mbox);
+        } else {
+            mbox = findIt.value();
+        }
+
+        mbox->readIndexData();
+        indexInvalidated = mbox->hasIndexData();
+    } else if (folderType == MaildirFolder) {
+        MaildirPtr md = getOrCreateMaildirPtr(path, false);
+
+        md->readIndexData();
+        indexInvalidated = md->hasIndexData();
+    }
+
+    if (!parentDir.rename(fileInfo.absoluteFilePath(), targetFileInfo.fileName())) {
+        errorText = i18nc("@info:status", "Cannot rename folder %1",
+                          collection.name());
+        qCritical() << errorText << "FolderType=" << folderType;
+        q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+        return false;
+    }
+
+    if (subDirInfo.exists()) {
+        if (!parentDir.rename(subDirInfo.absoluteFilePath(), targetSubDirInfo.fileName())) {
+            errorText = i18nc("@info:status", "Cannot rename folder %1",
+                              collection.name());
+            qCritical() << errorText << "FolderType=" << folderType;
+            q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+
+            // try to recover the previous rename
+            parentDir.rename(targetFileInfo.absoluteFilePath(), fileInfo.fileName());
+            return false;
+        }
+    }
+
+    // update context hashes
+    updateContextHashes(fileInfo.absoluteFilePath(), targetFileInfo.absoluteFilePath());
+
+    Collection renamedCollection = collection;
+
+    // when renaming top level folder, change path of store
+    if (folderType == TopLevelFolder) {
+        // backup caches, setTopLevelCollection() clears them
+        const MBoxHash mboxes = mMBoxes;
+        const MaildirHash maildirs = mMaildirs;
+
+        q->setPath(targetFileInfo.absoluteFilePath());
+
+        // restore caches
+        mMBoxes = mboxes;
+        mMaildirs = maildirs;
+
+        renamedCollection = q->topLevelCollection();
+    } else {
+        renamedCollection.setRemoteId(collectionName);
+        renamedCollection.setName(collectionName);
+    }
+
+    // update collections in MBox contexts so they stay usable for purge
+    Q_FOREACH (const MBoxPtr &mbox, mMBoxes) {
+        if (mbox->mCollection.isValid()) {
+            MBoxPtr updatedMBox = mbox;
+            updatedMBox->mCollection = updateMBoxCollectionTree(mbox->mCollection, collection, renamedCollection);
+        }
+    }
+
+    if (indexInvalidated) {
+        const QVariant var = QVariant::fromValue<Collection::List>(Collection::List() << renamedCollection);
+        job->setProperty("onDiskIndexInvalidated", var);
+    }
+
+    q->notifyCollectionsProcessed(Collection::List() << renamedCollection);
+    return true;
+}
+
+bool MixedMaildirStore::Private::visit(FileStore::CollectionMoveJob *job)
+{
+    QString errorText;
+
+    const Collection moveCollection = job->collection();
+    const Collection targetCollection = job->targetParent();
+
+    QString movePath;
+    const FolderType moveFolderType = folderForCollection(moveCollection, movePath, errorText);
+    if (moveFolderType == InvalidFolder || moveFolderType == TopLevelFolder) {
+        errorText = i18nc("@info:status", "Cannot move folder %1 from folder %2 to folder %3",
+                          moveCollection.name(), moveCollection.parentCollection().name(), targetCollection.name());
+        qCritical() << errorText << "FolderType=" << moveFolderType;
+        q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+        return false;
+    }
+
+//   qCDebug(MIXEDMAILDIR_LOG) << "moveCollection" << moveCollection.remoteId()
+//                                    << "movePath=" << movePath
+//                                    << "moveType=" << moveFolderType;
+
+    QString targetPath;
+    const FolderType targetFolderType = folderForCollection(targetCollection, targetPath, errorText);
+    if (targetFolderType == InvalidFolder) {
+        errorText = i18nc("@info:status", "Cannot move folder %1 from folder %2 to folder %3",
+                          moveCollection.name(), moveCollection.parentCollection().name(), targetCollection.name());
+        qCritical() << errorText << "FolderType=" << targetFolderType;
+        q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+        return false;
+    }
+
+//   qCDebug(MIXEDMAILDIR_LOG) << "targetCollection" << targetCollection.remoteId()
+//                                    << "targetPath=" << targetPath
+//                                    << "targetType=" << targetFolderType;
+
+    const QFileInfo targetSubDirInfo(Maildir::subDirPathForFolderPath(targetPath));
+
+    // if target is not the top level folder, make sure the sub folder directory exists
+    if (targetFolderType != TopLevelFolder) {
+        if (!targetSubDirInfo.exists()) {
+            QDir topDir(q->path());
+            if (!topDir.mkpath(targetSubDirInfo.absoluteFilePath())) {
+                errorText = i18nc("@info:status", "Cannot move folder %1 from folder %2 to folder %3",
+                                  moveCollection.name(), moveCollection.parentCollection().name(), targetCollection.name());
+                qCritical() << errorText << "MoveFolderType=" << moveFolderType
+                            << "TargetFolderType=" << targetFolderType;
+                q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+                return false;
+            }
+        }
+    }
+
+    bool indexInvalidated = false;
+    QString movedPath;
+
+    if (moveFolderType == MBoxFolder) {
+        // TODO would be nice if getOrCreateMBoxPtr() could be used instead, like below for Maildir
+        MBoxPtr mbox;
+        MBoxHash::const_iterator findIt = mMBoxes.constFind(movePath);
+        if (findIt == mMBoxes.constEnd()) {
+            mbox = MBoxPtr(new MBoxContext);
+            if (!mbox->load(movePath)) {
+                qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "Failed to load mbox" << movePath;
+            }
+
+            mbox->mCollection = moveCollection;
+            mMBoxes.insert(movePath, mbox);
+        } else {
+            mbox = findIt.value();
+        }
+
+        mbox->readIndexData();
+        indexInvalidated = mbox->hasIndexData();
+
+        const QFileInfo moveFileInfo(movePath);
+        const QFileInfo moveSubDirInfo(Maildir::subDirPathForFolderPath(movePath));
+        const QFileInfo targetFileInfo(targetPath);
+
+        QDir targetDir(targetFolderType == TopLevelFolder ?
+                       targetPath : Maildir::subDirPathForFolderPath(targetPath));
+        if (targetDir.exists(moveFileInfo.fileName()) ||
+                !targetDir.rename(moveFileInfo.absoluteFilePath(), moveFileInfo.fileName())) {
+            errorText = i18nc("@info:status", "Cannot move folder %1 from folder %2 to folder %3",
+                              moveCollection.name(), moveCollection.parentCollection().name(), targetCollection.name());
+            qCritical() << errorText << "MoveFolderType=" << moveFolderType
+                        << "TargetFolderType=" << targetFolderType;
+            q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+            return false;
+        }
+
+        if (moveSubDirInfo.exists()) {
+            if (targetDir.exists(moveSubDirInfo.fileName()) ||
+                    !targetDir.rename(moveSubDirInfo.absoluteFilePath(), moveSubDirInfo.fileName())) {
+                errorText = i18nc("@info:status", "Cannot move folder %1 from folder %2 to folder %3",
+                                  moveCollection.name(), moveCollection.parentCollection().name(), targetCollection.name());
+                qCritical() << errorText << "MoveFolderType=" << moveFolderType
+                            << "TargetFolderType=" << targetFolderType;
+
+                // try to revert the other rename
+                QDir sourceDir(moveFileInfo.absolutePath());
+                sourceDir.cdUp();
+                sourceDir.rename(targetFileInfo.absoluteFilePath(), moveFileInfo.fileName());
+                q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+                return false;
+            }
+        }
+
+        movedPath = QFileInfo(targetDir, moveFileInfo.fileName()).absoluteFilePath();
+    } else {
+        MaildirPtr md = getOrCreateMaildirPtr(movePath, false);
+
+        md->readIndexData();
+        indexInvalidated = md->hasIndexData();
+
+        Maildir moveMd(movePath, false);
+
+        // for moving purpose we can treat the MBox target's subDirPath like a top level maildir
+        Maildir targetMd;
+        if (targetFolderType == MBoxFolder) {
+            targetMd = Maildir(targetSubDirInfo.absoluteFilePath(), true);
+        } else {
+            targetMd = Maildir(targetPath, targetFolderType == TopLevelFolder);
+        }
+
+        if (!moveMd.moveTo(targetMd)) {
+            errorText = i18nc("@info:status", "Cannot move folder %1 from folder %2 to folder %3",
+                              moveCollection.name(), moveCollection.parentCollection().name(), targetCollection.name());
+            qCritical() << errorText << "MoveFolderType=" << moveFolderType
+                        << "TargetFolderType=" << targetFolderType;
+            q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+            return false;
+        }
+
+        movedPath = targetMd.subFolder(moveCollection.remoteId()).path();
+    }
+
+    // update context hashes
+    updateContextHashes(movePath, movedPath);
+
+    Collection movedCollection = moveCollection;
+    movedCollection.setParentCollection(targetCollection);
+
+    // update collections in MBox contexts so they stay usable for purge
+    Q_FOREACH (const MBoxPtr &mbox, mMBoxes) {
+        if (mbox->mCollection.isValid()) {
+            MBoxPtr updatedMBox = mbox;
+            updatedMBox->mCollection = updateMBoxCollectionTree(mbox->mCollection, moveCollection, movedCollection);
+        }
+    }
+
+    if (indexInvalidated) {
+        const QVariant var = QVariant::fromValue<Collection::List>(Collection::List() << movedCollection);
+        job->setProperty("onDiskIndexInvalidated", var);
+    }
+
+    q->notifyCollectionsProcessed(Collection::List() << movedCollection);
+    return true;
+}
+
+bool MixedMaildirStore::Private::visit(FileStore::ItemCreateJob *job)
+{
+    QString path;
+    QString errorText;
+
+    const FolderType folderType = folderForCollection(job->collection(), path, errorText);
+    if (folderType == InvalidFolder || folderType == TopLevelFolder) {
+        errorText = i18nc("@info:status", "Cannot add emails to folder %1",
+                          job->collection().name());
+        qCritical() << errorText << "FolderType=" << folderType;
+        q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+        return false;
+    }
+
+    Item item = job->item();
+
+    if (folderType == MBoxFolder) {
+        MBoxPtr mbox;
+        MBoxHash::const_iterator findIt = mMBoxes.constFind(path);
+        if (findIt == mMBoxes.constEnd()) {
+            mbox = MBoxPtr(new MBoxContext);
+            if (!mbox->load(path)) {
+                errorText = i18nc("@info:status", "Cannot add emails to folder %1",
+                                  job->collection().name());
+                qCritical() << errorText << "FolderType=" << folderType;
+                q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+                return false;
+            }
+
+            mbox->mCollection = job->collection();
+            mMBoxes.insert(path, mbox);
+        } else {
+            mbox = findIt.value();
+        }
+
+        // make sure to read the index (if available) before modifying the data, which would
+        // make the index invalid
+        mbox->readIndexData();
+
+        // if there is index data now, we let the job creator know that the on-disk index
+        // became invalid
+        if (mbox->hasIndexData()) {
+            const QVariant var = QVariant::fromValue<Collection::List>(Collection::List() << job->collection());
+            job->setProperty("onDiskIndexInvalidated", var);
+        }
+
+        qint64 result = mbox->appendEntry(item.payload<KMime::Message::Ptr>());
+        if (result < 0) {
+            errorText = i18nc("@info:status", "Cannot add emails to folder %1",
+                              job->collection().name());
+            qCritical() << errorText << "FolderType=" << folderType;
+            q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+            return false;
+        }
+        mbox->save();
+        item.setRemoteId(QString::number(result));
+    } else {
+        MaildirPtr mdPtr;
+        MaildirHash::const_iterator findIt = mMaildirs.constFind(path);
+        if (findIt == mMaildirs.constEnd()) {
+            mdPtr = MaildirPtr(new MaildirContext(path, false));
+            mMaildirs.insert(path, mdPtr);
+        } else {
+            mdPtr = findIt.value();
+        }
+
+        // make sure to read the index (if available) before modifying the data, which would
+        // make the index invalid
+        mdPtr->readIndexData();
+
+        // if there is index data now, we let the job creator know that the on-disk index
+        // became invalid
+        if (mdPtr->hasIndexData()) {
+            const QVariant var = QVariant::fromValue<Collection::List>(Collection::List() << job->collection());
+            job->setProperty("onDiskIndexInvalidated", var);
+        }
+
+        const QString result = mdPtr->addEntry(item.payload<KMime::Message::Ptr>()->encodedContent());
+        if (result.isEmpty()) {
+            errorText = i18nc("@info:status", "Cannot add emails to folder %1",
+                              job->collection().name());
+            qCritical() << errorText << "FolderType=" << folderType;
+            q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+            return false;
+        }
+
+        item.setRemoteId(result);
+    }
+
+    item.setParentCollection(job->collection());
+    q->notifyItemsProcessed(Item::List() << item);
+    return true;
+}
+
+bool MixedMaildirStore::Private::visit(FileStore::ItemDeleteJob *job)
+{
+    const Item item = job->item();
+    const Collection collection = item.parentCollection();
+    QString path;
+    QString errorText;
+
+    const FolderType folderType = folderForCollection(collection, path, errorText);
+    if (folderType == InvalidFolder || folderType == TopLevelFolder) {
+        errorText = i18nc("@info:status", "Cannot remove emails from folder %1",
+                          collection.name());
+        qCritical() << errorText << "FolderType=" << folderType;
+        q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+        return false;
+    }
+
+    if (folderType == MBoxFolder) {
+        MBoxPtr mbox;
+        MBoxHash::const_iterator findIt = mMBoxes.constFind(path);
+        if (findIt == mMBoxes.constEnd()) {
+            mbox = MBoxPtr(new MBoxContext);
+            if (!mbox->load(path)) {
+                errorText = i18nc("@info:status", "Cannot remove emails from folder %1",
+                                  collection.name());
+                qCritical() << errorText << "FolderType=" << folderType;
+                q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+                return false;
+            }
+
+            mMBoxes.insert(path, mbox);
+        } else {
+            mbox = findIt.value();
+        }
+
+        bool ok = false;
+        qint64 offset = item.remoteId().toLongLong(&ok);
+        if (!ok || offset < 0 || !mbox->isValidOffset(offset)) {
+            errorText = i18nc("@info:status", "Cannot remove emails from folder %1",
+                              collection.name());
+            qCritical() << errorText << "FolderType=" << folderType;
+            q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+            return false;
+        }
+
+        mbox->mCollection = collection;
+        mbox->deleteEntry(offset);
+        job->setProperty("compactStore", true);
+    } else {
+        MaildirPtr mdPtr;
+        MaildirHash::const_iterator findIt = mMaildirs.constFind(path);
+        if (findIt == mMaildirs.constEnd()) {
+            mdPtr = MaildirPtr(new MaildirContext(path, false));
+
+            mMaildirs.insert(path, mdPtr);
+        } else {
+            mdPtr = findIt.value();
+        }
+
+        // make sure to read the index (if available) before modifying the data, which would
+        // make the index invalid
+        mdPtr->readIndexData();
+
+        // if there is index data now, we let the job creator know that the on-disk index
+        // became invalid
+        if (mdPtr->hasIndexData()) {
+            const QVariant var = QVariant::fromValue<Collection::List>(Collection::List() << collection);
+            job->setProperty("onDiskIndexInvalidated", var);
+        }
+
+        if (!mdPtr->removeEntry(item.remoteId())) {
+            errorText = i18nc("@info:status", "Cannot remove emails from folder %1",
+                              collection.name());
+            qCritical() << errorText << "FolderType=" << folderType;
+            q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+            return false;
+        }
+    }
+
+    q->notifyItemsProcessed(Item::List() << item);
+    return true;
+}
+
+bool MixedMaildirStore::Private::visit(FileStore::ItemFetchJob *job)
+{
+    ItemFetchScope scope = job->fetchScope();
+    const bool includeBody = scope.fullPayload() ||
+                             scope.payloadParts().contains(MessagePart::Body);
+    const bool includeHeaders = scope.payloadParts().contains(MessagePart::Header) ||
+                                scope.payloadParts().contains(MessagePart::Envelope);
+
+    const bool fetchSingleItem = job->collection().remoteId().isEmpty();
+    const Collection collection = fetchSingleItem ? job->item().parentCollection() : job->collection();
+
+    QString path;
+    QString errorText;
+    Q_ASSERT(!collection.remoteId().isEmpty());
+    const FolderType folderType = folderForCollection(collection, path, errorText);
+
+    if (folderType == InvalidFolder) {
+        qCritical() << errorText << "collection:" << job->collection();
+        q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+        return false;
+    }
+
+    if (folderType == MBoxFolder) {
+        MBoxHash::iterator findIt = mMBoxes.find(path);
+        if (findIt == mMBoxes.end() || !fetchSingleItem) {
+            MBoxPtr mbox = findIt != mMBoxes.end() ? findIt.value() : MBoxPtr(new MBoxContext);
+            if (!mbox->load(path)) {
+                errorText = i18nc("@info:status", "Failed to load MBox folder %1", path);
+                qCritical() << errorText << "collection=" << collection;
+                q->notifyError(FileStore::Job::InvalidJobContext, errorText);   // TODO should be a different error code
+                if (findIt != mMBoxes.end()) {
+                    mMBoxes.erase(findIt);
+                }
+                return false;
+            }
+
+            if (findIt == mMBoxes.end()) {
+                findIt = mMBoxes.insert(path, mbox);
+            }
+        }
+
+        Item::List items;
+        if (fetchSingleItem) {
+            items << job->item();
+        } else {
+            listCollection(job, findIt.value(), collection, items);
+        }
+
+        Item::List::iterator it    = items.begin();
+        Item::List::iterator endIt = items.end();
+        for (; it != endIt; ++it) {
+            if (!fillItem(findIt.value(), includeHeaders, includeBody, *it)) {
+                const QString errorText =
+                    i18nc("@info:status", "Error while reading mails from folder %1", collection.name());
+                q->notifyError(FileStore::Job::InvalidJobContext, errorText);   // TODO should be a different error code
+                qCritical() << "Failed to read item" << (*it).remoteId() << "in MBox file" << path;
+                return false;
+            }
+        }
+
+        if (!items.isEmpty()) {
+            q->notifyItemsProcessed(items);
+        }
+    } else {
+        MaildirPtr mdPtr;
+        MaildirHash::const_iterator mdIt = mMaildirs.constFind(path);
+        if (mdIt == mMaildirs.constEnd()) {
+            mdPtr = MaildirPtr(new MaildirContext(path, folderType == TopLevelFolder));
+            mMaildirs.insert(path, mdPtr);
+        } else {
+            mdPtr = mdIt.value();
+        }
+
+        if (!mdPtr->isValid(errorText)) {
+            errorText = i18nc("@info:status", "Failed to load Maildirs folder %1", path);
+            qCritical() << errorText << "collection=" << collection;
+            q->notifyError(FileStore::Job::InvalidJobContext, errorText);   // TODO should be a different error code
+            return false;
+        }
+
+        Item::List items;
+        if (fetchSingleItem) {
+            items << job->item();
+        } else {
+            listCollection(job, mdPtr, collection, items);
+        }
+
+        Item::List::iterator it    = items.begin();
+        Item::List::iterator endIt = items.end();
+        for (; it != endIt; ++it) {
+            if (!fillItem(mdPtr, includeHeaders, includeBody, *it)) {
+                const QString errorText =
+                    i18nc("@info:status", "Error while reading mails from folder %1", collection.name());
+                q->notifyError(FileStore::Job::InvalidJobContext, errorText);   // TODO should be a different error code
+                qCritical() << "Failed to read item" << (*it).remoteId() << "in Maildir" << path;
+                return false;
+            }
+        }
+
+        if (!items.isEmpty()) {
+            q->notifyItemsProcessed(items);
+        }
+    }
+
+    return true;
+}
+
+bool MixedMaildirStore::Private::visit(FileStore::ItemModifyJob *job)
+{
+    const QSet<QByteArray> parts = job->parts();
+    bool payloadChanged = false;
+    bool flagsChanged = false;
+    Q_FOREACH (const QByteArray &part, parts)  {
+        if (part.startsWith("PLD:")) {
+            payloadChanged = true;
+        }
+        if (part.contains("FLAGS")) {
+            flagsChanged = true;
+        }
+    }
+
+    const bool nothingChanged = (!payloadChanged && !flagsChanged);
+    const bool payloadChangedButIgnored = payloadChanged && job->ignorePayload();
+    const bool ignoreModifyIfValid = nothingChanged ||
+                                     (payloadChangedButIgnored && !flagsChanged);
+
+    Item item = job->item();
+    const Collection collection = item.parentCollection();
+    QString path;
+    QString errorText;
+
+    const FolderType folderType = folderForCollection(collection, path, errorText);
+    if (folderType == InvalidFolder || folderType == TopLevelFolder) {
+        errorText = i18nc("@info:status", "Cannot modify emails in folder %1",
+                          collection.name());
+        qCritical() << errorText << "FolderType=" << folderType;
+        q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+        return false;
+    }
+
+    if (folderType == MBoxFolder) {
+        MBoxPtr mbox;
+        MBoxHash::const_iterator findIt = mMBoxes.constFind(path);
+        if (findIt == mMBoxes.constEnd()) {
+            mbox = MBoxPtr(new MBoxContext);
+            if (!mbox->load(path)) {
+                errorText = i18nc("@info:status", "Cannot modify emails in folder %1",
+                                  collection.name());
+                qCritical() << errorText << "FolderType=" << folderType;
+                q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+                return false;
+            }
+
+            mMBoxes.insert(path, mbox);
+        } else {
+            mbox = findIt.value();
+        }
+
+        bool ok = false;
+        qint64 offset = item.remoteId().toLongLong(&ok);
+        if (!ok || offset < 0 || !mbox->isValidOffset(offset)) {
+            errorText = i18nc("@info:status", "Cannot modify emails in folder %1",
+                              collection.name());
+            qCritical() << errorText << "FolderType=" << folderType;
+            q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+            return false;
+        }
+
+        // if we can ignore payload, or we have nothing else to change, then we are finished
+        if (ignoreModifyIfValid) {
+            qCDebug(MIXEDMAILDIR_LOG) << "ItemModifyJob for item" << item.remoteId()
+                                      << "in collection" << collection.remoteId()
+                                      << "skipped: nothing of interest changed (" << nothingChanged
+                                      << ") or only payload changed but should be ignored ("
+                                      << (payloadChanged && !flagsChanged && job->ignorePayload())
+                                      << "). Modified parts:" << parts;
+            q->notifyItemsProcessed(Item::List() << job->item());
+            return true;
+        }
+
+        // mbox can only change payload, ignore any other change
+        if (!payloadChanged) {
+            q->notifyItemsProcessed(Item::List() << item);
+            return true;
+        }
+
+        // make sure to read the index (if available) before modifying the data, which would
+        // make the index invalid
+        mbox->readIndexData();
+
+        // if there is index data now, we let the job creator know that the on-disk index
+        // became invalid
+        if (mbox->hasIndexData()) {
+            const QVariant var = QVariant::fromValue<Collection::List>(Collection::List() << collection);
+            job->setProperty("onDiskIndexInvalidated", var);
+        }
+
+        qint64 newOffset = mbox->appendEntry(item.payload<KMime::Message::Ptr>());
+        if (newOffset < 0) {
+            errorText = i18nc("@info:status", "Cannot modify emails in folder %1",
+                              collection.name());
+            qCritical() << errorText << "FolderType=" << folderType;
+            q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+            return false;
+        }
+
+        if (newOffset > 0) {
+            mbox->mCollection = collection;
+            mbox->deleteEntry(offset);
+            job->setProperty("compactStore", true);
+        }
+        mbox->save();
+        item.setRemoteId(QString::number(newOffset));
+    } else {
+        MaildirPtr mdPtr;
+        MaildirHash::const_iterator findIt = mMaildirs.constFind(path);
+        if (findIt == mMaildirs.constEnd()) {
+            mdPtr = MaildirPtr(new MaildirContext(path, false));
+            mMaildirs.insert(path, mdPtr);
+        } else {
+            mdPtr = findIt.value();
+        }
+
+        if (!mdPtr->isValidEntry(item.remoteId())) {
+            errorText = i18nc("@info:status", "Cannot modify emails in folder %1",
+                              collection.name());
+            qCritical() << errorText << "FolderType=" << folderType;
+            q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+            return false;
+        }
+
+        // if we can ignore payload, or we have nothing else to change, then we are finished
+        if (ignoreModifyIfValid) {
+            qCDebug(MIXEDMAILDIR_LOG) << "ItemModifyJob for item" << item.remoteId()
+                                      << "in collection" << collection.remoteId()
+                                      << "skipped: nothing of interest changed (" << nothingChanged
+                                      << ") or only payload changed but should be ignored ("
+                                      << (payloadChanged && !flagsChanged && job->ignorePayload())
+                                      << "). Modified parts:" << parts;
+            q->notifyItemsProcessed(Item::List() << job->item());
+            return true;
+        }
+
+        // make sure to read the index (if available) before modifying the data, which would
+        // make the index invalid
+        mdPtr->readIndexData();
+
+        // if there is index data now, we let the job creator know that the on-disk index
+        // became invalid
+        if (mdPtr->hasIndexData()) {
+            const QVariant var = QVariant::fromValue<Collection::List>(Collection::List() << collection);
+            job->setProperty("onDiskIndexInvalidated", var);
+        }
+
+        QString newKey = item.remoteId();
+        if (flagsChanged) {
+            Maildir md(mdPtr->maildir());
+            newKey = md.changeEntryFlags(item.remoteId(), item.flags());
+            if (newKey.isEmpty()) {
+                errorText = i18nc("@info:status", "Cannot modify emails in folder %1. %2",
+                                  collection.name(), md.lastError());
+                qCritical() << errorText << "FolderType=" << folderType;
+                q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+                return false;
+            }
+            item.setRemoteId(newKey);
+        }
+
+        if (payloadChanged) {
+            mdPtr->writeEntry(newKey, item.payload<KMime::Message::Ptr>()->encodedContent());
+        }
+    }
+
+    q->notifyItemsProcessed(Item::List() << item);
+    return true;
+}
+
+bool MixedMaildirStore::Private::visit(FileStore::ItemMoveJob *job)
+{
+    QString errorText;
+
+    QString sourcePath;
+    const Collection sourceCollection = job->item().parentCollection();
+    const FolderType sourceFolderType = folderForCollection(sourceCollection, sourcePath, errorText);
+    if (sourceFolderType == InvalidFolder || sourceFolderType == TopLevelFolder) {
+        errorText = i18nc("@info:status", "Cannot move emails from folder %1",
+                          sourceCollection.name());
+        qCritical() << errorText << "FolderType=" << sourceFolderType;
+        q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+        return false;
+    }
+
+//   qCDebug(MIXEDMAILDIR_LOG) << "sourceCollection" << sourceCollection.remoteId()
+//                                    << "sourcePath=" << sourcePath
+//                                    << "sourceType=" << sourceFolderType;
+
+    QString targetPath;
+    const Collection targetCollection = job->targetParent();
+    const FolderType targetFolderType = folderForCollection(targetCollection, targetPath, errorText);
+    if (targetFolderType == InvalidFolder || targetFolderType == TopLevelFolder) {
+        errorText = i18nc("@info:status", "Cannot move emails to folder %1",
+                          targetCollection.name());
+        qCritical() << errorText << "FolderType=" << targetFolderType;
+        q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+        return false;
+    }
+
+//   qCDebug(MIXEDMAILDIR_LOG) << "targetCollection" << targetCollection.remoteId()
+//                                    << "targetPath=" << targetPath
+//                                    << "targetType=" << targetFolderType;
+
+    Item item = job->item();
+
+    if (sourceFolderType == MBoxFolder) {
+        /*    qCDebug(MIXEDMAILDIR_LOG) << "source is MBox";*/
+        bool ok = false;
+        quint64 offset = item.remoteId().toULongLong(&ok);
+        if (!ok) {
+            errorText = i18nc("@info:status", "Cannot move emails from folder %1",
+                              sourceCollection.name());
+            qCritical() << errorText << "FolderType=" << sourceFolderType;
+            q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+            return false;
+        }
+
+        MBoxPtr mbox;
+        MBoxHash::const_iterator findIt = mMBoxes.constFind(sourcePath);
+        if (findIt == mMBoxes.constEnd()) {
+            mbox = MBoxPtr(new MBoxContext);
+            if (!mbox->load(sourcePath)) {
+                errorText = i18nc("@info:status", "Cannot move emails to folder %1",
+                                  sourceCollection.name());
+                qCritical() << errorText << "FolderType=" << sourceFolderType;
+                q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+                return false;
+            }
+
+            mbox->mCollection = sourceCollection;
+            mMBoxes.insert(sourcePath, mbox);
+        } else {
+            mbox = findIt.value();
+        }
+
+        if (!mbox->isValidOffset(offset)) {
+            errorText = i18nc("@info:status", "Cannot move emails from folder %1",
+                              sourceCollection.name());
+            qCritical() << errorText << "FolderType=" << sourceFolderType;
+            q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+            return false;
+        }
+
+        if (!item.hasPayload<KMime::Message::Ptr>() ||
+                !item.loadedPayloadParts().contains(MessagePart::Body)) {
+            if (!fillItem(mbox, true, true, item)) {
+                errorText = i18nc("@info:status", "Cannot move email from folder %1",
+                                  sourceCollection.name());
+                qCritical() << errorText << "FolderType=" << sourceFolderType;
+                q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+                return false;
+            }
+        }
+
+        Collection::List collections;
+
+        // make sure to read the index (if available) before modifying the data, which would
+        // make the index invalid
+        mbox->readIndexData();
+
+        if (mbox->hasIndexData()) {
+            collections << sourceCollection;
+        }
+
+        if (targetFolderType == MBoxFolder) {
+            /*      qCDebug(MIXEDMAILDIR_LOG) << "target is MBox";*/
+            MBoxPtr targetMBox;
+            MBoxHash::const_iterator findIt = mMBoxes.constFind(targetPath);
+            if (findIt == mMBoxes.constEnd()) {
+                targetMBox = MBoxPtr(new MBoxContext);
+                if (!targetMBox->load(targetPath)) {
+                    errorText = i18nc("@info:status", "Cannot move emails to folder %1",
+                                      targetCollection.name());
+                    qCritical() << errorText << "FolderType=" << targetFolderType;
+                    q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+                    return false;
+                }
+
+                targetMBox->mCollection = targetCollection;
+                mMBoxes.insert(targetPath, targetMBox);
+            } else {
+                targetMBox = findIt.value();
+            }
+
+            // make sure to read the index (if available) before modifying the data, which would
+            // make the index invalid
+            targetMBox->readIndexData();
+
+            // if there is index data now, we let the job creator know that the on-disk index
+            // became invalid
+            if (targetMBox->hasIndexData()) {
+                collections << targetCollection;
+            }
+
+            qint64 remoteId = targetMBox->appendEntry(item.payload<KMime::Message::Ptr>());
+            if (remoteId < 0) {
+                errorText = i18nc("@info:status", "Cannot move emails to folder %1",
+                                  targetCollection.name());
+                qCritical() << errorText << "FolderType=" << targetFolderType;
+                q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+                return false;
+            }
+
+            if (!targetMBox->save()) {
+                errorText = i18nc("@info:status", "Cannot move emails to folder %1",
+                                  targetCollection.name());
+                qCritical() << errorText << "FolderType=" << targetFolderType;
+                q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+                return false;
+            }
+
+            item.setRemoteId(QString::number(remoteId));
+        } else {
+            /*      qCDebug(MIXEDMAILDIR_LOG) << "target is Maildir";*/
+            MaildirPtr targetMdPtr = getOrCreateMaildirPtr(targetPath, false);
+
+            // make sure to read the index (if available) before modifying the data, which would
+            // make the index invalid
+            targetMdPtr->readIndexData();
+
+            // if there is index data now, we let the job creator know that the on-disk index
+            // became invalid
+            if (targetMdPtr->hasIndexData()) {
+                collections << targetCollection;
+            }
+
+            const QString remoteId = targetMdPtr->addEntry(mbox->readRawEntry(offset));
+            if (remoteId.isEmpty()) {
+                errorText = i18nc("@info:status", "Cannot move email from folder %1 to folder %2",
+                                  sourceCollection.name(), targetCollection.name());
+                qCritical() << errorText << "SourceFolderType=" << sourceFolderType << "TargetFolderType=" << targetFolderType;
+                q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+                return false;
+            }
+
+            item.setRemoteId(remoteId);
+        }
+
+        if (!collections.isEmpty()) {
+            const QVariant var = QVariant::fromValue<Collection::List>(collections);
+            job->setProperty("onDiskIndexInvalidated", var);
+        }
+
+        mbox->mCollection = sourceCollection;
+        mbox->deleteEntry(offset);
+        job->setProperty("compactStore", true);
+    } else {
+        /*    qCDebug(MIXEDMAILDIR_LOG) << "source is Maildir";*/
+        MaildirPtr sourceMdPtr = getOrCreateMaildirPtr(sourcePath, false);
+
+        if (!sourceMdPtr->isValidEntry(item.remoteId())) {
+            errorText = i18nc("@info:status", "Cannot move email from folder %1",
+                              sourceCollection.name());
+            qCritical() << errorText << "FolderType=" << sourceFolderType;
+            q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+            return false;
+        }
+
+        Collection::List collections;
+
+        // make sure to read the index (if available) before modifying the data, which would
+        // make the index invalid
+        sourceMdPtr->readIndexData();
+
+        // if there is index data now, we let the job creator know that the on-disk index
+        // became invalid
+        if (sourceMdPtr->hasIndexData()) {
+            collections << sourceCollection;
+        }
+
+        if (targetFolderType == MBoxFolder) {
+            /*      qCDebug(MIXEDMAILDIR_LOG) << "target is MBox";*/
+            if (!item.hasPayload<KMime::Message::Ptr>() ||
+                    !item.loadedPayloadParts().contains(MessagePart::Body)) {
+                if (!fillItem(sourceMdPtr, true, true, item)) {
+                    errorText = i18nc("@info:status", "Cannot move email from folder %1",
+                                      sourceCollection.name());
+                    qCritical() << errorText << "FolderType=" << sourceFolderType;
+                    q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+                    return false;
+                }
+            }
+
+            MBoxPtr mbox;
+            MBoxHash::const_iterator findIt = mMBoxes.constFind(targetPath);
+            if (findIt == mMBoxes.constEnd()) {
+                mbox = MBoxPtr(new MBoxContext);
+                if (!mbox->load(targetPath)) {
+                    errorText = i18nc("@info:status", "Cannot move emails to folder %1",
+                                      targetCollection.name());
+                    qCritical() << errorText << "FolderType=" << targetFolderType;
+                    q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+                    return false;
+                }
+
+                mbox->mCollection = targetCollection;
+                mMBoxes.insert(targetPath, mbox);
+            } else {
+                mbox = findIt.value();
+            }
+
+            // make sure to read the index (if available) before modifying the data, which would
+            // make the index invalid
+            mbox->readIndexData();
+
+            // if there is index data now, we let the job creator know that the on-disk index
+            // became invalid
+            if (mbox->hasIndexData()) {
+                collections << targetCollection;
+            }
+
+            const qint64 remoteId = mbox->appendEntry(item.payload<KMime::Message::Ptr>());
+            if (remoteId < 0) {
+                errorText = i18nc("@info:status", "Cannot move emails to folder %1",
+                                  targetCollection.name());
+                qCritical() << errorText << "FolderType=" << targetFolderType;
+                q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+                return false;
+            }
+            sourceMdPtr->removeEntry(item.remoteId());
+
+            mbox->save();
+            item.setRemoteId(QString::number(remoteId));
+        } else {
+            /*      qCDebug(MIXEDMAILDIR_LOG) << "target is Maildir";*/
+            MaildirPtr targetMdPtr = getOrCreateMaildirPtr(targetPath, false);
+
+            // make sure to read the index (if available) before modifying the data, which would
+            // make the index invalid
+            targetMdPtr->readIndexData();
+
+            // if there is index data now, we let the job creator know that the on-disk index
+            // became invalid
+            if (targetMdPtr->hasIndexData()) {
+                collections << targetCollection;
+            }
+
+            const QString remoteId = sourceMdPtr->moveEntryTo(item.remoteId(), *targetMdPtr);
+            if (remoteId.isEmpty()) {
+                errorText = i18nc("@info:status", "Cannot move email from folder %1 to folder %2",
+                                  sourceCollection.name(), targetCollection.name());
+                qCritical() << errorText << "SourceFolderType=" << sourceFolderType << "TargetFolderType=" << targetFolderType;
+                q->notifyError(FileStore::Job::InvalidJobContext, errorText);
+                return false;
+            }
+
+            item.setRemoteId(remoteId);
+        }
+
+        if (!collections.isEmpty()) {
+            const QVariant var = QVariant::fromValue<Collection::List>(collections);
+            job->setProperty("onDiskIndexInvalidated", var);
+        }
+    }
+
+    item.setParentCollection(targetCollection);
+    q->notifyItemsProcessed(Item::List() << item);
+    return true;
+}
+
+bool MixedMaildirStore::Private::visit(FileStore::StoreCompactJob *job)
+{
+    Q_UNUSED(job);
+
+    Collection::List collections;
+
+    MBoxHash::const_iterator it    = mMBoxes.constBegin();
+    MBoxHash::const_iterator endIt = mMBoxes.constEnd();
+    for (; it != endIt; ++it) {
+        MBoxPtr mbox = it.value();
+
+        if (!mbox->hasDeletedOffsets()) {
+            continue;
+        }
+
+        // make sure to read the index (if available) before modifying the data, which would
+        // make the index invalid
+        mbox->readIndexData();
+
+        QList<KMBox::MBoxEntry::Pair> movedEntries;
+        const int result = mbox->purge(movedEntries);
+        if (result > 0) {
+            if (movedEntries.count() > 0) {
+                qint64 revision = mbox->mCollection.remoteRevision().toLongLong();
+                qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "purge of" << mbox->mCollection.name() << "caused item move: oldRevision="
+                                                  << revision << "(stored)," << mbox->mRevision << "(local)";
+                revision = qMax(revision, mbox->mRevision) + 1;
+
+                const QString remoteRevision = QString::number(revision);
+
+                Collection collection = mbox->mCollection;
+                collection.attribute<FileStore::EntityCompactChangeAttribute>(Collection::AddIfMissing)->setRemoteRevision(remoteRevision);
+
+                q->notifyCollectionsProcessed(Collection::List() << collection);
+
+                mbox->mCollection.setRemoteRevision(remoteRevision);
+                mbox->mRevision = revision;
+            }
+
+            Item::List items;
+            items.reserve(movedEntries.count());
+            Q_FOREACH (const KMBox::MBoxEntry::Pair &offsetPair, movedEntries) {
+                const QString oldRemoteId(QString::number(offsetPair.first.messageOffset()));
+                const QString newRemoteId(QString::number(offsetPair.second.messageOffset()));
+
+                Item item;
+                item.setRemoteId(oldRemoteId);
+                item.setParentCollection(mbox->mCollection);
+                item.attribute<FileStore::EntityCompactChangeAttribute>(Item::AddIfMissing)->setRemoteId(newRemoteId);
+
+                items << item;
+            }
+
+            // if there is index data, we let the job creator know that the on-disk index
+            // became invalid
+            if (mbox->hasIndexData()) {
+                collections << mbox->mCollection;
+            }
+
+            if (!items.isEmpty()) {
+                q->notifyItemsProcessed(items);
+            }
+        }
+    }
+
+    if (!collections.isEmpty()) {
+        const QVariant var = QVariant::fromValue<Collection::List>(collections);
+        job->setProperty("onDiskIndexInvalidated", var);
+    }
+
+    return true;
+}
+
+MixedMaildirStore::MixedMaildirStore() : FileStore::AbstractLocalStore(), d(new Private(this))
+{
+}
+
+MixedMaildirStore::~MixedMaildirStore()
+{
+    delete d;
+}
+
+void MixedMaildirStore::setTopLevelCollection(const Collection &collection)
+{
+    QStringList contentMimeTypes;
+    contentMimeTypes << Collection::mimeType();
+
+    Collection::Rights rights;
+    // TODO check if read-only?
+    rights = Collection::CanCreateCollection | Collection::CanChangeCollection | Collection::CanDeleteCollection;
+
+    CachePolicy cachePolicy;
+    cachePolicy.setInheritFromParent(false);
+    cachePolicy.setLocalParts(QStringList() << QLatin1String(MessagePart::Envelope));
+    cachePolicy.setSyncOnDemand(true);
+    cachePolicy.setCacheTimeout(1);
+
+    Collection modifiedCollection = collection;
+    modifiedCollection.setContentMimeTypes(contentMimeTypes);
+    modifiedCollection.setRights(rights);
+    modifiedCollection.setParentCollection(Collection::root());
+    modifiedCollection.setCachePolicy(cachePolicy);
+
+    // clear caches
+    d->mMBoxes.clear();
+    d->mMaildirs.clear();
+
+    FileStore::AbstractLocalStore::setTopLevelCollection(modifiedCollection);
+}
+
+void MixedMaildirStore::processJob(FileStore::Job *job)
+{
+    if (!job->accept(d)) {
+        // check that an error has been set
+        if (job->error() == 0 || job->errorString().isEmpty()) {
+            qCritical() << "visitor did not set either error code or error string when returning false";
+            Q_ASSERT(job->error() == 0 || job->errorString().isEmpty());
+        }
+    } else {
+        // check that no error has been set
+        if (job->error() != 0 || !job->errorString().isEmpty()) {
+            qCritical() << "visitor did set either error code or error string when returning true";
+            Q_ASSERT(job->error() != 0 || !job->errorString().isEmpty());
+        }
+    }
+}
+
+void MixedMaildirStore::checkCollectionMove(FileStore::CollectionMoveJob *job, int &errorCode, QString &errorText) const
+{
+    // check if the target is not the collection itself or one if its children
+    Collection targetCollection = job->targetParent();
+    while (targetCollection.isValid()) {
+        if (targetCollection == job->collection()) {
+            errorCode = FileStore::Job::InvalidJobContext;
+            errorText = i18nc("@info:status", "Cannot move folder %1 into one of its own subfolder tree", job->collection().name());
+            return;
+        }
+
+        targetCollection = targetCollection.parentCollection();
+    }
+}
+
+void MixedMaildirStore::checkItemCreate(FileStore::ItemCreateJob *job, int &errorCode, QString &errorText) const
+{
+    if (!job->item().hasPayload<KMime::Message::Ptr>()) {
+        errorCode = FileStore::Job::InvalidJobContext;
+        errorText = i18nc("@info:status", "Cannot add email to folder %1 because there is no email content", job->collection().name());
+    }
+}
+
+void MixedMaildirStore::checkItemModify(FileStore::ItemModifyJob *job, int &errorCode, QString &errorText) const
+{
+    if (!job->ignorePayload() && !job->item().hasPayload<KMime::Message::Ptr>()) {
+        errorCode = FileStore::Job::InvalidJobContext;
+        errorText = i18nc("@info:status", "Cannot modify email in folder %1 because there is no email content", job->item().parentCollection().name());
+    }
+}
+
+void MixedMaildirStore::checkItemFetch(FileStore::ItemFetchJob *job, int &errorCode, QString &errorText) const
+{
+    Q_UNUSED(errorCode);
+    Q_UNUSED(errorText);
+    const bool fetchSingleItem = job->collection().remoteId().isEmpty();
+    if (fetchSingleItem) {
+        Collection coll = job->item().parentCollection();
+        Q_ASSERT(!coll.remoteId().isEmpty());
+    }
+}
+
diff --git a/resources/mixedmaildir/mixedmaildirstore.h b/resources/mixedmaildir/mixedmaildirstore.h
new file mode 100644 (file)
index 0000000..82bba47
--- /dev/null
@@ -0,0 +1,53 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef MIXEDMAILDIRSTORE_H
+#define MIXEDMAILDIRSTORE_H
+
+#include "abstractlocalstore.h"
+
+class MixedMaildirStore : public Akonadi::FileStore::AbstractLocalStore
+{
+    Q_OBJECT
+
+public:
+    MixedMaildirStore();
+
+    ~MixedMaildirStore();
+
+protected:
+    void setTopLevelCollection(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    void processJob(Akonadi::FileStore::Job *job) Q_DECL_OVERRIDE;
+
+    void checkCollectionMove(Akonadi::FileStore::CollectionMoveJob *job, int &errorCode, QString &errorText) const Q_DECL_OVERRIDE;
+
+    void checkItemCreate(Akonadi::FileStore::ItemCreateJob *job, int &errorCode, QString &errorText) const Q_DECL_OVERRIDE;
+
+    void checkItemModify(Akonadi::FileStore::ItemModifyJob *job, int &errorCode, QString &errorText) const Q_DECL_OVERRIDE;
+
+    void checkItemFetch(Akonadi::FileStore::ItemFetchJob *job, int &errorCode, QString &errorText) const Q_DECL_OVERRIDE;
+
+private:
+    class Private;
+    Private *const d;
+};
+
+#endif
+
diff --git a/resources/mixedmaildir/retrieveitemsjob.cpp b/resources/mixedmaildir/retrieveitemsjob.cpp
new file mode 100644 (file)
index 0000000..bcd1441
--- /dev/null
@@ -0,0 +1,366 @@
+/*  This file is part of the KDE project
+    Copyright (c) 2011 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "retrieveitemsjob.h"
+
+#include "mixedmaildirstore.h"
+#include "mixedmaildir_debug.h"
+
+#include "filestore/itemfetchjob.h"
+
+#include <akonadi/kmime/messageparts.h>
+#include <akonadi/kmime/messagestatus.h>
+
+#include <AkonadiCore/collection.h>
+#include <AkonadiCore/collectionmodifyjob.h>
+#include <AkonadiCore/item.h>
+#include <AkonadiCore/itemcreatejob.h>
+#include <AkonadiCore/itemdeletejob.h>
+#include <AkonadiCore/itemfetchjob.h>
+#include <AkonadiCore/itemfetchscope.h>
+#include <AkonadiCore/itemmodifyjob.h>
+#include <AkonadiCore/transactionsequence.h>
+#include <AkonadiCore/vectorhelper.h>
+
+#include "mixedmaildirresource_debug.h"
+
+#include <QDateTime>
+#include <QQueue>
+#include <QVariant>
+
+using namespace Akonadi;
+
+enum {
+    MaxItemCreateJobs = 100,
+    MaxItemModifyJobs = 100
+};
+
+class RetrieveItemsJob::Private
+{
+    RetrieveItemsJob *const q;
+
+public:
+    Private(RetrieveItemsJob *parent, const Collection &collection, MixedMaildirStore *store)
+        : q(parent), mCollection(collection), mStore(store),
+          mTransaction(Q_NULLPTR), mHighestModTime(-1), mNumItemCreateJobs(0), mNumItemModifyJobs(0)
+    {
+    }
+
+    TransactionSequence *transaction()
+    {
+        if (!mTransaction) {
+            mTransaction = new TransactionSequence(q);
+            mTransaction->setAutomaticCommittingEnabled(false);
+            connect(mTransaction, SIGNAL(result(KJob*)),
+                    q, SLOT(transactionResult(KJob*)));
+        }
+        return mTransaction;
+    }
+
+public:
+    const Collection mCollection;
+    MixedMaildirStore *const mStore;
+    TransactionSequence *mTransaction;
+
+    QHash<QString, Item> mServerItemsByRemoteId;
+
+    QQueue<Item> mNewItems;
+    QQueue<Item> mChangedItems;
+    Item::List mAvailableItems;
+    Item::List mItemsMarkedAsDeleted;
+    qint64 mHighestModTime;
+    int mNumItemCreateJobs;
+    int mNumItemModifyJobs;
+
+public: // slots
+    void akonadiFetchResult(KJob *job);
+    void transactionResult(KJob *job);
+    void storeListResult(KJob *);
+    void processNewItem();
+    void fetchNewResult(KJob *);
+    void processChangedItem();
+    void fetchChangedResult(KJob *);
+    void itemCreateJobResult(KJob *);
+    void itemModifyJobResult(KJob *);
+};
+
+void RetrieveItemsJob::Private::itemCreateJobResult(KJob *job)
+{
+    if (job->error()) {
+        qCCritical(MIXEDMAILDIR_LOG) << "Error running ItemCreateJob: " << job->errorText();
+    }
+
+    mNumItemCreateJobs--;
+    QMetaObject::invokeMethod(q, "processNewItem", Qt::QueuedConnection);
+}
+
+void RetrieveItemsJob::Private::itemModifyJobResult(KJob *job)
+{
+    if (job->error()) {
+        qCCritical(MIXEDMAILDIR_LOG) << "Error running ItemModifyJob: " << job->errorText();
+    }
+
+    mNumItemModifyJobs--;
+    QMetaObject::invokeMethod(q, "processChangedItem", Qt::QueuedConnection);
+}
+
+void RetrieveItemsJob::Private::akonadiFetchResult(KJob *job)
+{
+    if (job->error() != 0) {
+        return;    // handled by base class
+    }
+
+    ItemFetchJob *itemFetch = qobject_cast<ItemFetchJob *>(job);
+    Q_ASSERT(itemFetch != 0);
+
+    Item::List items = itemFetch->items();
+    itemFetch->clearItems(); // save memory
+    qCDebug(MIXEDMAILDIR_LOG) << "Akonadi fetch got" << items.count() << "items";
+
+    mServerItemsByRemoteId.reserve(items.size());
+    for (int i = 0; i < items.count(); ++i) {
+        Item &item = items[i];
+        // items without remoteId have not been written to the resource yet
+        if (!item.remoteId().isEmpty()) {
+            // set the parent collection (with all ancestors) in every item
+            item.setParentCollection(mCollection);
+            mServerItemsByRemoteId.insert(item.remoteId(), item);
+        }
+    }
+
+    qCDebug(MIXEDMAILDIR_LOG) << "of which" << mServerItemsByRemoteId.count() << "have remoteId";
+
+    FileStore::ItemFetchJob *storeFetch = mStore->fetchItems(mCollection);
+    // just basic items, no data
+
+    connect(storeFetch, SIGNAL(result(KJob*)), q, SLOT(storeListResult(KJob*)));
+}
+
+void RetrieveItemsJob::Private::storeListResult(KJob *job)
+{
+    qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "storeList->error=" << job->error();
+    FileStore::ItemFetchJob *storeList = qobject_cast<FileStore::ItemFetchJob *>(job);
+    Q_ASSERT(storeList != 0);
+
+    if (storeList->error() != 0) {
+        q->setError(storeList->error());
+        q->setErrorText(storeList->errorText());
+        q->emitResult();
+        return;
+    }
+
+    // if some items have tags, we need to complete the retrieval and schedule tagging
+    // to a later time so we can then fetch the items to get their Akonadi URLs
+    // forward the property to this instance so the resource can take care of that
+    const QVariant var = storeList->property("remoteIdToTagList");
+    if (var.isValid()) {
+        q->setProperty("remoteIdToTagList", var);
+    }
+
+    const qint64 collectionTimestamp = mCollection.remoteRevision().toLongLong();
+
+    const Item::List storedItems = storeList->items();
+    Q_FOREACH (const Item &item, storedItems) {
+        // messages marked as deleted have been deleted from mbox files but never got purged
+        Akonadi::MessageStatus status;
+        status.setStatusFromFlags(item.flags());
+        if (status.isDeleted()) {
+            mItemsMarkedAsDeleted << item;
+            continue;
+        }
+
+        mAvailableItems << item;
+
+        const QHash<QString, Item>::iterator it = mServerItemsByRemoteId.find(item.remoteId());
+        if (it == mServerItemsByRemoteId.end()) {
+            // item not in server items -> new
+            mNewItems << item;
+        } else {
+            // item both on server and in store, check modification time
+            const QDateTime modTime = item.modificationTime();
+            if (!modTime.isValid() || modTime.toMSecsSinceEpoch() > collectionTimestamp) {
+                mChangedItems << it.value();
+            }
+
+            // remove from hash so only no longer existing items remain
+            mServerItemsByRemoteId.erase(it);
+        }
+    }
+
+    qCDebug(MIXEDMAILDIR_LOG) << "Store fetch got" << storedItems.count() << "items"
+                              << "of which" << mNewItems.count() << "are new and" << mChangedItems.count()
+                              << "are changed and" << mServerItemsByRemoteId.count()
+                              << "need to be removed";
+
+    // all items remaining in mServerItemsByRemoteId are no longer in the store
+
+    if (!mServerItemsByRemoteId.isEmpty()) {
+        ItemDeleteJob *deleteJob = new ItemDeleteJob(Akonadi::valuesToVector(mServerItemsByRemoteId), transaction());
+        transaction()->setIgnoreJobFailure(deleteJob);
+    }
+
+    processNewItem();
+}
+
+void RetrieveItemsJob::Private::processNewItem()
+{
+    if (mNewItems.isEmpty()) {
+        processChangedItem();
+        return;
+    }
+
+    const Item item = mNewItems.dequeue();
+    FileStore::ItemFetchJob *storeFetch = mStore->fetchItem(item);
+    storeFetch->fetchScope().fetchPayloadPart(MessagePart::Envelope);
+
+    connect(storeFetch, SIGNAL(result(KJob*)), q, SLOT(fetchNewResult(KJob*)));
+}
+
+void RetrieveItemsJob::Private::fetchNewResult(KJob *job)
+{
+    FileStore::ItemFetchJob *fetchJob = qobject_cast<FileStore::ItemFetchJob *>(job);
+    Q_ASSERT(fetchJob != 0);
+
+    if (fetchJob->items().count() != 1) {
+        const Item item = fetchJob->item();
+        qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "Store fetch for new item" << item.remoteId()
+                                            << "in collection" << item.parentCollection().id()
+                                            << "," << item.parentCollection().remoteId()
+                                            << "did not return the expected item. error="
+                                            << fetchJob->error() << "," << fetchJob->errorText();
+        processNewItem();
+        return;
+    }
+
+    const Item item = fetchJob->items().at(0);
+    const QDateTime modTime = item.modificationTime();
+    if (modTime.isValid()) {
+        mHighestModTime = qMax(modTime.toMSecsSinceEpoch(), mHighestModTime);
+    }
+
+    ItemCreateJob *itemCreate = new ItemCreateJob(item, mCollection, transaction());
+    mNumItemCreateJobs++;
+    connect(itemCreate, SIGNAL(result(KJob*)), q, SLOT(itemCreateJobResult(KJob*)));
+
+    if (mNumItemCreateJobs < MaxItemCreateJobs) {
+        QMetaObject::invokeMethod(q, "processNewItem", Qt::QueuedConnection);
+    }
+}
+
+void RetrieveItemsJob::Private::processChangedItem()
+{
+    if (mChangedItems.isEmpty()) {
+        if (!mTransaction) {
+            // no jobs created here -> done
+            q->emitResult();
+            return;
+        }
+
+        if (mHighestModTime > -1) {
+            Collection collection(mCollection);
+            collection.setRemoteRevision(QString::number(mHighestModTime));
+            CollectionModifyJob *job = new CollectionModifyJob(collection, transaction());
+            transaction()->setIgnoreJobFailure(job);
+        }
+        transaction()->commit();
+        return;
+    }
+
+    const Item item = mChangedItems.dequeue();
+    FileStore::ItemFetchJob *storeFetch = mStore->fetchItem(item);
+    storeFetch->fetchScope().fetchPayloadPart(MessagePart::Envelope);
+
+    connect(storeFetch, SIGNAL(result(KJob*)), q, SLOT(fetchChangedResult(KJob*)));
+}
+
+void RetrieveItemsJob::Private::fetchChangedResult(KJob *job)
+{
+    FileStore::ItemFetchJob *fetchJob = qobject_cast<FileStore::ItemFetchJob *>(job);
+    Q_ASSERT(fetchJob != 0);
+
+    if (fetchJob->items().count() != 1) {
+        const Item item = fetchJob->item();
+        qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "Store fetch for changed item" << item.remoteId()
+                                            << "in collection" << item.parentCollection().id()
+                                            << "," << item.parentCollection().remoteId()
+                                            << "did not return the expected item. error="
+                                            << fetchJob->error() << "," << fetchJob->errorText();
+        processChangedItem();
+        return;
+    }
+
+    const Item item = fetchJob->items().at(0);
+    const QDateTime modTime = item.modificationTime();
+    if (modTime.isValid()) {
+        mHighestModTime = qMax(modTime.toMSecsSinceEpoch(), mHighestModTime);
+    }
+
+    ItemModifyJob *itemModify = new ItemModifyJob(item, transaction());
+    connect(itemModify, SIGNAL(result(KJob*)), q, SLOT(itemModifyJobResult(KJob*)));
+    mNumItemModifyJobs++;
+    if (mNumItemModifyJobs < MaxItemModifyJobs) {
+        QMetaObject::invokeMethod(q, "processChangedItem", Qt::QueuedConnection);
+    }
+}
+
+void RetrieveItemsJob::Private::transactionResult(KJob *job)
+{
+    if (job->error() != 0) {
+        return;    // handled by base class
+    }
+
+    q->emitResult();
+}
+
+RetrieveItemsJob::RetrieveItemsJob(const Akonadi::Collection &collection, MixedMaildirStore *store, QObject *parent)
+    : Job(parent), d(new Private(this, collection, store))
+{
+    Q_ASSERT(d->mCollection.isValid());
+    Q_ASSERT(!d->mCollection.remoteId().isEmpty());
+    Q_ASSERT(d->mStore != 0);
+}
+
+RetrieveItemsJob::~RetrieveItemsJob()
+{
+    delete d;
+}
+
+Collection RetrieveItemsJob::collection() const
+{
+    return d->mCollection;
+}
+
+Item::List RetrieveItemsJob::availableItems() const
+{
+    return d->mAvailableItems;
+}
+
+Item::List RetrieveItemsJob::itemsMarkedAsDeleted() const
+{
+    return d->mItemsMarkedAsDeleted;
+}
+
+void RetrieveItemsJob::doStart()
+{
+    ItemFetchJob *job = new Akonadi::ItemFetchJob(d->mCollection, this);
+    connect(job, SIGNAL(result(KJob*)), this, SLOT(akonadiFetchResult(KJob*)));
+}
+
+#include "moc_retrieveitemsjob.cpp"
+
diff --git a/resources/mixedmaildir/retrieveitemsjob.h b/resources/mixedmaildir/retrieveitemsjob.h
new file mode 100644 (file)
index 0000000..43d58a2
--- /dev/null
@@ -0,0 +1,70 @@
+/*  This file is part of the KDE project
+    Copyright (c) 2011 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef MIXEDMAILDIR_RETRIEVEITEMSJOB_H
+#define MIXEDMAILDIR_RETRIEVEITEMSJOB_H
+
+#include <AkonadiCore/Item>
+#include <AkonadiCore/Job>
+
+namespace Akonadi
+{
+class Collection;
+}
+
+class MixedMaildirStore;
+
+/**
+ * Used to implement ResourceBase::retrieveItems() for MixedMail Resource.
+ * This completely bypasses ItemSync in order to achieve maximum performance.
+ */
+class RetrieveItemsJob : public Akonadi::Job
+{
+    Q_OBJECT
+public:
+    RetrieveItemsJob(const Akonadi::Collection &collection, MixedMaildirStore *store, QObject *parent = Q_NULLPTR);
+
+    ~RetrieveItemsJob();
+
+    Akonadi::Collection collection() const;
+
+    Akonadi::Item::List availableItems() const;
+
+    Akonadi::Item::List itemsMarkedAsDeleted() const;
+
+protected:
+    void doStart() Q_DECL_OVERRIDE;
+
+private:
+    class Private;
+    Private *const d;
+
+    Q_PRIVATE_SLOT(d, void itemModifyJobResult(KJob *))
+    Q_PRIVATE_SLOT(d, void itemCreateJobResult(KJob *))
+    Q_PRIVATE_SLOT(d, void akonadiFetchResult(KJob *))
+    Q_PRIVATE_SLOT(d, void transactionResult(KJob *))
+    Q_PRIVATE_SLOT(d, void storeListResult(KJob *))
+    Q_PRIVATE_SLOT(d, void processNewItem())
+    Q_PRIVATE_SLOT(d, void fetchNewResult(KJob *))
+    Q_PRIVATE_SLOT(d, void processChangedItem())
+    Q_PRIVATE_SLOT(d, void fetchChangedResult(KJob *))
+};
+
+#endif
+
diff --git a/resources/mixedmaildir/settings.kcfgc b/resources/mixedmaildir/settings.kcfgc
new file mode 100644 (file)
index 0000000..789365e
--- /dev/null
@@ -0,0 +1,8 @@
+File=mixedmaildirresource.kcfg
+ClassName=Settings
+Mutators=true
+ItemAccessors=true
+SetUserTexts=true
+Singleton=true
+#IncludeFiles=
+GlobalEnums=true
diff --git a/resources/mixedmaildir/settings.ui b/resources/mixedmaildir/settings.ui
new file mode 100644 (file)
index 0000000..8c60e10
--- /dev/null
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <author>Till Adam &lt;adam@kde.org&gt;</author>
+ <class>ConfigDialog</class>
+ <widget class="QWidget" name="ConfigDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>290</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Mail Directory Settings</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QLabel" name="label">
+     <property name="text">
+      <string>Select the folder containing the maildir information:</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="KUrlRequester" name="kcfg_Path"/>
+   </item>
+   <item>
+    <widget class="QCheckBox" name="kcfg_ReadOnly">
+     <property name="text">
+      <string>Open in read-only mode</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="statusLabel">
+     <property name="text">
+      <string/>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <spacer name="verticalSpacer">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>13</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KUrlRequester</class>
+   <extends>QFrame</extends>
+   <header>kurlrequester.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/resources/openxchange/CMakeLists.txt b/resources/openxchange/CMakeLists.txt
new file mode 100644 (file)
index 0000000..78ce4eb
--- /dev/null
@@ -0,0 +1,63 @@
+
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_openxchange_resource\")
+
+set( openxchangeresource_SRCS
+  oxa/connectiontestjob.cpp
+  oxa/contactutils.cpp
+  oxa/davmanager.cpp
+  oxa/davutils.cpp
+  oxa/folder.cpp
+  oxa/foldercreatejob.cpp
+  oxa/folderdeletejob.cpp
+  oxa/foldermodifyjob.cpp
+  oxa/foldermovejob.cpp
+  oxa/folderrequestjob.cpp
+  oxa/foldersrequestdeltajob.cpp
+  oxa/foldersrequestjob.cpp
+  oxa/folderutils.cpp
+  oxa/incidenceutils.cpp
+  oxa/object.cpp
+  oxa/objectcreatejob.cpp
+  oxa/objectdeletejob.cpp
+  oxa/objectmodifyjob.cpp
+  oxa/objectmovejob.cpp
+  oxa/objectrequestjob.cpp
+  oxa/objectsrequestdeltajob.cpp
+  oxa/objectsrequestjob.cpp
+  oxa/objectutils.cpp
+  oxa/oxutils.cpp
+  oxa/oxerrors.cpp
+  oxa/updateusersjob.cpp
+  oxa/user.cpp
+  oxa/users.cpp
+  oxa/useridrequestjob.cpp
+  oxa/usersrequestjob.cpp
+
+  configdialog.cpp
+  openxchangeresource.cpp
+)
+
+ki18n_wrap_ui( openxchangeresource_SRCS configdialog.ui )
+
+install( FILES openxchangeresource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents" )
+
+kconfig_add_kcfg_files(openxchangeresource_SRCS settings.kcfgc)
+kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/openxchangeresource.kcfg org.kde.Akonadi.OpenXchange.Settings)
+qt5_add_dbus_adaptor(openxchangeresource_SRCS
+  ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.OpenXchange.Settings.xml settings.h Settings
+)
+
+add_executable(akonadi_openxchange_resource ${openxchangeresource_SRCS})
+
+if( APPLE )
+  set_target_properties(akonadi_openxchange_resource PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../Info.plist.template)
+  set_target_properties(akonadi_openxchange_resource PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.OpenXchange")
+  set_target_properties(akonadi_openxchange_resource PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi OpenXchange Resource")
+endif ()
+
+
+target_link_libraries(akonadi_openxchange_resource KF5::AkonadiAgentBase KF5::AkonadiCore KF5::Contacts     KF5::KIOCore KF5::CalendarCore  KF5::AkonadiContact )
+
+install(TARGETS akonadi_openxchange_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
+
+add_subdirectory(icons)
diff --git a/resources/openxchange/Messages.sh b/resources/openxchange/Messages.sh
new file mode 100644 (file)
index 0000000..3f07585
--- /dev/null
@@ -0,0 +1,3 @@
+#!/bin/sh
+$EXTRACTRC *.ui *.kcfg >> rc.cpp
+$XGETTEXT *.cpp -o $podir/akonadi_openxchange_resource.pot
diff --git a/resources/openxchange/configdialog.cpp b/resources/openxchange/configdialog.cpp
new file mode 100644 (file)
index 0000000..a8a2abf
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "configdialog.h"
+
+#include "oxa/connectiontestjob.h"
+#include "settings.h"
+#include "ui_configdialog.h"
+
+#include <kaboutdata.h>
+#include <kaboutapplicationdialog.h>
+#include <kconfigdialogmanager.h>
+#include <kmessagebox.h>
+#include <kwindowsystem.h>
+#include <KLocalizedString>
+#include <QDialogButtonBox>
+#include <KConfigGroup>
+#include <QPushButton>
+#include <QVBoxLayout>
+
+ConfigDialog::ConfigDialog(WId windowId)
+    : QDialog()
+{
+    if (windowId) {
+        KWindowSystem::setMainWindow(this, windowId);
+    }
+
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    QWidget *mainWidget = new QWidget(this);
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    setLayout(mainLayout);
+    mainLayout->addWidget(mainWidget);
+    QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
+    okButton->setDefault(true);
+    okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
+    QPushButton *user1Button = new QPushButton;
+    buttonBox->addButton(user1Button, QDialogButtonBox::ActionRole);
+    connect(buttonBox, &QDialogButtonBox::accepted, this, &ConfigDialog::accept);
+    connect(buttonBox, &QDialogButtonBox::rejected, this, &ConfigDialog::reject);
+    mainLayout->addWidget(buttonBox);
+    user1Button->setText(i18n("About..."));
+
+    setWindowTitle(i18n("Open-Xchange Configuration"));
+
+    Ui::ConfigDialog ui;
+    ui.setupUi(mainWidget);
+
+    ui.kcfg_BaseUrl->setWhatsThis(i18n("Enter the http or https URL to your Open-Xchange installation here."));
+    ui.kcfg_Username->setWhatsThis(i18n("Enter the username of your Open-Xchange account here."));
+    ui.kcfg_Password->setWhatsThis(i18n("Enter the password of your Open-Xchange account here."));
+
+    mServerEdit = ui.kcfg_BaseUrl;
+    mUserEdit = ui.kcfg_Username;
+    mPasswordEdit = ui.kcfg_Password;
+    mCheckConnectionButton = ui.checkConnectionButton;
+
+    mManager = new KConfigDialogManager(this, Settings::self());
+    mManager->updateWidgets();
+
+    connect(okButton, &QPushButton::clicked, this, &ConfigDialog::save);
+    connect(user1Button, &QPushButton::clicked, this, &ConfigDialog::showAboutDialog);
+    connect(mServerEdit, &KLineEdit::textChanged, this, &ConfigDialog::updateButtonState);
+    connect(mUserEdit, &KLineEdit::textChanged, this, &ConfigDialog::updateButtonState);
+    connect(mCheckConnectionButton, &QPushButton::clicked, this, &ConfigDialog::checkConnection);
+
+    resize(QSize(410, 200));
+}
+
+void ConfigDialog::save()
+{
+    mManager->updateSettings();
+    Settings::self()->save();
+}
+
+void ConfigDialog::showAboutDialog()
+{
+    KAboutData aboutData(QStringLiteral("ox"), i18n("Open-Xchange"), QStringLiteral("0.1"),
+                         i18n("Akonadi Open-Xchange Resource"),
+                         KAboutLicense::LGPL,
+                         i18n("(c) 2009 by Tobias Koenig (credativ GmbH)"));
+    aboutData.addAuthor(i18n("Tobias Koenig"), i18n("Current maintainer"), QStringLiteral("tokoe@kde.org"));
+    aboutData.addCredit(i18n("credativ GmbH"), i18n("Funded and supported"), QStringLiteral("http://www.credativ.com"));
+
+    KAboutApplicationDialog dlg(aboutData, this);
+    dlg.exec();
+}
+
+void ConfigDialog::updateButtonState()
+{
+    mCheckConnectionButton->setEnabled(!mServerEdit->text().isEmpty() && !mUserEdit->text().isEmpty());
+}
+
+void ConfigDialog::checkConnection()
+{
+    OXA::ConnectionTestJob *job = new OXA::ConnectionTestJob(mServerEdit->text(), mUserEdit->text(),
+            mPasswordEdit->text(), this);
+    connect(job, &OXA::ConnectionTestJob::result, this, &ConfigDialog::checkConnectionJobFinished);
+    job->start();
+
+    QApplication::setOverrideCursor(Qt::WaitCursor);
+}
+
+void ConfigDialog::checkConnectionJobFinished(KJob *job)
+{
+    QApplication::restoreOverrideCursor();
+
+    if (job->error()) {
+        KMessageBox::error(this, i18n("Unable to connect: %1", job->errorText()), i18n("Connection error"));
+    } else {
+        KMessageBox::information(this, i18n("Tested connection successfully."), i18n("Connection success"));
+    }
+}
diff --git a/resources/openxchange/configdialog.h b/resources/openxchange/configdialog.h
new file mode 100644 (file)
index 0000000..6526614
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef CONFIGDIALOG_H
+#define CONFIGDIALOG_H
+
+#include <QDialog>
+
+class KConfigDialogManager;
+class KJob;
+class KLineEdit;
+
+class ConfigDialog : public QDialog
+{
+    Q_OBJECT
+
+public:
+    explicit ConfigDialog(WId windowId);
+
+private Q_SLOTS:
+    void save();
+    void showAboutDialog();
+    void updateButtonState();
+    void checkConnection();
+    void checkConnectionJobFinished(KJob *);
+
+private:
+    KConfigDialogManager *mManager;
+    KLineEdit *mServerEdit;
+    KLineEdit *mUserEdit;
+    KLineEdit *mPasswordEdit;
+    QPushButton *mCheckConnectionButton;
+};
+
+#endif
diff --git a/resources/openxchange/configdialog.ui b/resources/openxchange/configdialog.ui
new file mode 100644 (file)
index 0000000..a24248f
--- /dev/null
@@ -0,0 +1,156 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ConfigDialog</class>
+ <widget class="QWidget" name="ConfigDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>410</width>
+    <height>202</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Open-Xchange Configuration</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QGroupBox" name="groupBox">
+     <property name="title">
+      <string>Connection</string>
+     </property>
+     <property name="checkable">
+      <bool>false</bool>
+     </property>
+     <layout class="QFormLayout" name="formLayout">
+      <item row="0" column="0">
+       <widget class="QLabel" name="label_BaseUrl">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="minimumSize">
+         <size>
+          <width>120</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Server URL:</string>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="1">
+       <widget class="KLineEdit" name="kcfg_BaseUrl">
+        <property name="toolTip">
+         <string>The URL of the Open-Xchange server, should be something like https://myserver.org/</string>
+        </property>
+        <property name="whatsThis">
+         <string>The URL of the Open-Xchange server, should be something like https://myserver.org/</string>
+        </property>
+        <property name="placeholderText">
+         <string notr="true">https://myserver.org</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="0">
+       <widget class="QLabel" name="label_Username">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="minimumSize">
+         <size>
+          <width>120</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Username:</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="1">
+       <widget class="KLineEdit" name="kcfg_Username">
+        <property name="toolTip">
+         <string>The username that is used to log into the Open-Xchange server</string>
+        </property>
+        <property name="whatsThis">
+         <string>The username that is used to log into the Open-Xchange server</string>
+        </property>
+       </widget>
+      </item>
+      <item row="2" column="0">
+       <widget class="QLabel" name="label_Password">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="minimumSize">
+         <size>
+          <width>120</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Password:</string>
+        </property>
+       </widget>
+      </item>
+      <item row="2" column="1">
+       <widget class="KLineEdit" name="kcfg_Password">
+        <property name="toolTip">
+         <string>The password that is used to log into the Open-Xchange server</string>
+        </property>
+        <property name="whatsThis">
+         <string>The password that is used to log into the Open-Xchange server</string>
+        </property>
+        <property name="echoMode">
+         <enum>QLineEdit::Password</enum>
+        </property>
+       </widget>
+      </item>
+      <item row="3" column="1">
+       <widget class="QPushButton" name="checkConnectionButton">
+        <property name="enabled">
+         <bool>false</bool>
+        </property>
+        <property name="text">
+         <string>Test Connection...</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KLineEdit</class>
+   <extends>QLineEdit</extends>
+   <header>klineedit.h</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/resources/openxchange/icons/128-apps-ox.png b/resources/openxchange/icons/128-apps-ox.png
new file mode 100644 (file)
index 0000000..20b83dd
Binary files /dev/null and b/resources/openxchange/icons/128-apps-ox.png differ
diff --git a/resources/openxchange/icons/16-apps-ox.png b/resources/openxchange/icons/16-apps-ox.png
new file mode 100644 (file)
index 0000000..294859c
Binary files /dev/null and b/resources/openxchange/icons/16-apps-ox.png differ
diff --git a/resources/openxchange/icons/32-apps-ox.png b/resources/openxchange/icons/32-apps-ox.png
new file mode 100644 (file)
index 0000000..b3f21c9
Binary files /dev/null and b/resources/openxchange/icons/32-apps-ox.png differ
diff --git a/resources/openxchange/icons/48-apps-ox.png b/resources/openxchange/icons/48-apps-ox.png
new file mode 100644 (file)
index 0000000..a56f708
Binary files /dev/null and b/resources/openxchange/icons/48-apps-ox.png differ
diff --git a/resources/openxchange/icons/64-apps-ox.png b/resources/openxchange/icons/64-apps-ox.png
new file mode 100644 (file)
index 0000000..da2c12f
Binary files /dev/null and b/resources/openxchange/icons/64-apps-ox.png differ
diff --git a/resources/openxchange/icons/CMakeLists.txt b/resources/openxchange/icons/CMakeLists.txt
new file mode 100644 (file)
index 0000000..84c8b0a
--- /dev/null
@@ -0,0 +1 @@
+ecm_install_icons(ICONS 128-apps-ox.png  16-apps-ox.png  32-apps-ox.png  48-apps-ox.png  64-apps-ox.png DESTINATION ${KDE_INSTALL_ICONDIR} THEME hicolor)
diff --git a/resources/openxchange/openxchangeresource.cpp b/resources/openxchange/openxchangeresource.cpp
new file mode 100644 (file)
index 0000000..c298a9b
--- /dev/null
@@ -0,0 +1,1178 @@
+/*
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "openxchangeresource.h"
+
+#include "configdialog.h"
+#include "settingsadaptor.h"
+
+#include <cachepolicy.h>
+#include <changerecorder.h>
+#include <collectionfetchjob.h>
+#include <collectionfetchscope.h>
+#include <entitydisplayattribute.h>
+#include <itemfetchscope.h>
+#include <AkonadiCore/vectorhelper.h>
+
+#include <kcontacts/addressee.h>
+#include <KCalCore/Event>
+#include <KCalCore/Todo>
+#include <KLocalizedString>
+
+#include <oxa/davmanager.h>
+#include <oxa/oxerrors.h>
+#include <oxa/foldercreatejob.h>
+#include <oxa/folderdeletejob.h>
+#include <oxa/foldermodifyjob.h>
+#include <oxa/foldermovejob.h>
+#include <oxa/foldersrequestdeltajob.h>
+#include <oxa/foldersrequestjob.h>
+#include <oxa/objectcreatejob.h>
+#include <oxa/objectdeletejob.h>
+#include <oxa/objectmodifyjob.h>
+#include <oxa/objectmovejob.h>
+#include <oxa/objectrequestjob.h>
+#include <oxa/objectsrequestdeltajob.h>
+#include <oxa/objectsrequestjob.h>
+#include <oxa/updateusersjob.h>
+#include <oxa/users.h>
+#include <QStandardPaths>
+#include <QIcon>
+
+using namespace Akonadi;
+
+class RemoteInformation
+{
+public:
+    RemoteInformation(qlonglong objectId, OXA::Folder::Module module, const QString &lastModified)
+        : mObjectId(objectId),
+          mModule(module),
+          mLastModified(lastModified)
+    {
+    }
+
+    inline qlonglong objectId() const
+    {
+        return mObjectId;
+    }
+
+    inline OXA::Folder::Module module() const
+    {
+        return mModule;
+    }
+
+    inline QString lastModified() const
+    {
+        return mLastModified;
+    }
+
+    inline void setLastModified(const QString &lastModified)
+    {
+        mLastModified = lastModified;
+    }
+
+    static RemoteInformation load(const Item &item)
+    {
+        return loadImpl(item);
+    }
+
+    static RemoteInformation load(const Collection &collection)
+    {
+        return loadImpl(collection);
+    }
+
+    void store(Item &item) const
+    {
+        storeImpl(item);
+    }
+
+    void store(Collection &collection) const
+    {
+        storeImpl(collection);
+    }
+
+private:
+    template<typename T>
+    static inline RemoteInformation loadImpl(const T &entity)
+    {
+        const QStringList parts = entity.remoteRevision().split(QLatin1Char(':'), QString::KeepEmptyParts);
+
+        OXA::Folder::Module module = OXA::Folder::Unbound;
+
+        if (parts.count() > 0) {
+            if (parts.at(0) == QLatin1String("calendar")) {
+                module = OXA::Folder::Calendar;
+            } else if (parts.at(0) == QLatin1String("contacts")) {
+                module = OXA::Folder::Contacts;
+            } else if (parts.at(0) == QLatin1String("tasks")) {
+                module = OXA::Folder::Tasks;
+            } else {
+                module = OXA::Folder::Unbound;
+            }
+        }
+
+        QString lastModified = QStringLiteral("0");
+        if (parts.count() > 1) {
+            lastModified = parts.at(1);
+        }
+
+        return RemoteInformation(entity.remoteId().toLongLong(), module, lastModified);
+    }
+
+    template<typename T>
+    inline void storeImpl(T &entity) const
+    {
+        QString module;
+        switch (mModule) {
+        case OXA::Folder::Calendar:
+            module = QStringLiteral("calendar");
+            break;
+        case OXA::Folder::Contacts:
+            module = QStringLiteral("contacts");
+            break;
+        case OXA::Folder::Tasks:
+            module = QStringLiteral("tasks");
+            break;
+        case OXA::Folder::Unbound:
+            break;
+        }
+
+        QStringList parts;
+        parts.append(module);
+        parts.append(mLastModified);
+
+        entity.setRemoteId(QString::number(mObjectId));
+        entity.setRemoteRevision(parts.join(QStringLiteral(":")));
+    }
+
+    qlonglong mObjectId;
+    OXA::Folder::Module mModule;
+    QString mLastModified;
+};
+
+class ObjectsLastSync
+{
+public:
+    ObjectsLastSync()
+    {
+        if (!Settings::self()->objectsLastSync().isEmpty()) {
+            const QStringList pairs = Settings::self()->objectsLastSync().split(QLatin1Char(':'), QString::KeepEmptyParts);
+            foreach (const QString &pair, pairs) {
+                const QStringList entry = pair.split(QLatin1Char('='), QString::KeepEmptyParts);
+                mObjectsMap.insert(entry.at(0).toLongLong(), entry.at(1).toULongLong());
+            }
+        }
+    }
+
+    void save()
+    {
+        QStringList pairs;
+        pairs.reserve(mObjectsMap.count());
+
+        QMapIterator<qlonglong, qulonglong> it(mObjectsMap);
+        while (it.hasNext()) {
+            it.next();
+            pairs.append(QString::number(it.key()) + QLatin1Char('=') + QString::number(it.value()));
+        }
+
+        Settings::self()->setObjectsLastSync(pairs.join(QStringLiteral(":")));
+        Settings::self()->save();
+    }
+
+    qulonglong lastSync(qlonglong collectionId) const
+    {
+        if (!mObjectsMap.contains(collectionId)) {
+            return 0;
+        }
+
+        return mObjectsMap.value(collectionId);
+    }
+
+    void setLastSync(qlonglong collectionId, qulonglong timeStamp)
+    {
+        mObjectsMap.insert(collectionId, timeStamp);
+    }
+
+private:
+    QMap<qlonglong, qulonglong> mObjectsMap;
+};
+
+static Collection::Rights folderPermissionsToCollectionRights(const OXA::Folder &folder)
+{
+    const OXA::Folder::UserPermissions userPermissions = folder.userPermissions();
+
+    if (!userPermissions.contains(OXA::Users::self()->currentUserId())) {
+        // There are no rights given for us explicitly, so it is read-only
+        return Collection::ReadOnly;
+    } else {
+        const OXA::Folder::Permissions permissions = userPermissions.value(OXA::Users::self()->currentUserId());
+        Collection::Rights rights = Collection::ReadOnly;
+        switch (permissions.folderPermission()) {
+        case OXA::Folder::Permissions::FolderIsVisible: rights |= Collection::ReadOnly; break;
+        case OXA::Folder::Permissions::CreateObjects: rights |= Collection::CanCreateItem; break;
+        case OXA::Folder::Permissions::CreateSubfolders: // fallthrough
+        case OXA::Folder::Permissions::AdminPermission: rights |= (Collection::CanCreateItem | Collection::CanCreateCollection); break;
+        default: break;
+        }
+
+        if (permissions.objectWritePermission() != OXA::Folder::Permissions::NoWritePermission) {
+            rights |= Collection::CanChangeItem;
+            rights |= Collection::CanChangeCollection;
+        }
+
+        if (permissions.objectDeletePermission() != OXA::Folder::Permissions::NoDeletePermission) {
+            rights |= Collection::CanDeleteItem;
+            rights |= Collection::CanDeleteCollection;
+        }
+
+        return rights;
+    }
+}
+
+OpenXchangeResource::OpenXchangeResource(const QString &id)
+    : ResourceBase(id)
+{
+    // setup the resource
+    new SettingsAdaptor(Settings::self());
+    QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"),
+            Settings::self(), QDBusConnection::ExportAdaptors);
+
+    changeRecorder()->fetchCollection(true);
+    changeRecorder()->itemFetchScope().fetchFullPayload(true);
+    changeRecorder()->itemFetchScope().setAncestorRetrieval(ItemFetchScope::Parent);
+    changeRecorder()->collectionFetchScope().setAncestorRetrieval(CollectionFetchScope::Parent);
+
+    setName(i18n("Open-Xchange"));
+
+    OXA::Users::self()->init(identifier());
+
+    QUrl baseUrl = QUrl::fromLocalFile(Settings::self()->baseUrl());
+    baseUrl.setUserName(Settings::self()->username());
+    baseUrl.setPassword(Settings::self()->password());
+    OXA::DavManager::self()->setBaseUrl(baseUrl);
+
+    // Create the standard collections.
+    //
+    // There exists special OX folders (e.g. private, public, shared) that are not
+    // returned by a normal webdav listing, therefor we create them manually here.
+    // This is possible because the remote ids of these folders are fixed values from 1
+    // till 4.
+    mResourceCollection.setParentCollection(Collection::root());
+    const RemoteInformation resourceInformation(0, OXA::Folder::Unbound, QString());
+    resourceInformation.store(mResourceCollection);
+    mResourceCollection.setName(name());
+    mResourceCollection.setContentMimeTypes(QStringList() << Collection::mimeType());
+    mResourceCollection.setRights(Collection::ReadOnly);
+    EntityDisplayAttribute *attribute = mResourceCollection.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
+    attribute->setIconName(QStringLiteral("ox"));
+
+    Collection privateFolder;
+    privateFolder.setParentCollection(mResourceCollection);
+    const RemoteInformation privateFolderInformation(1, OXA::Folder::Unbound, QString());
+    privateFolderInformation.store(privateFolder);
+    privateFolder.setName(i18n("Private Folder"));
+    privateFolder.setContentMimeTypes(QStringList() << Collection::mimeType());
+    privateFolder.setRights(Collection::ReadOnly);
+
+    Collection publicFolder;
+    publicFolder.setParentCollection(mResourceCollection);
+    const RemoteInformation publicFolderInformation(2, OXA::Folder::Unbound, QString());
+    publicFolderInformation.store(publicFolder);
+    publicFolder.setName(i18n("Public Folder"));
+    publicFolder.setContentMimeTypes(QStringList() << Collection::mimeType());
+    publicFolder.setRights(Collection::ReadOnly);
+
+    Collection sharedFolder;
+    sharedFolder.setParentCollection(mResourceCollection);
+    const RemoteInformation sharedFolderInformation(3, OXA::Folder::Unbound, QString());
+    sharedFolderInformation.store(sharedFolder);
+    sharedFolder.setName(i18n("Shared Folder"));
+    sharedFolder.setContentMimeTypes(QStringList() << Collection::mimeType());
+    sharedFolder.setRights(Collection::ReadOnly);
+
+    Collection systemFolder;
+    systemFolder.setParentCollection(mResourceCollection);
+    const RemoteInformation systemFolderInformation(4, OXA::Folder::Unbound, QString());
+    systemFolderInformation.store(systemFolder);
+    systemFolder.setName(i18n("System Folder"));
+    systemFolder.setContentMimeTypes(QStringList() << Collection::mimeType());
+    systemFolder.setRights(Collection::ReadOnly);
+
+    // TODO: set cache policy depending on sync behaviour
+    Akonadi::CachePolicy cachePolicy;
+    cachePolicy.setInheritFromParent(false);
+    cachePolicy.setSyncOnDemand(false);
+    cachePolicy.setCacheTimeout(-1);
+    cachePolicy.setIntervalCheckTime(5);
+    mResourceCollection.setCachePolicy(cachePolicy);
+
+    mStandardCollectionsMap.insert(0, mResourceCollection);
+    mStandardCollectionsMap.insert(1, privateFolder);
+    mStandardCollectionsMap.insert(2, publicFolder);
+    mStandardCollectionsMap.insert(3, sharedFolder);
+    mStandardCollectionsMap.insert(4, systemFolder);
+
+    mCollectionsMap = mStandardCollectionsMap;
+
+    if (Settings::self()->useIncrementalUpdates()) {
+        syncCollectionsRemoteIdCache();
+    }
+}
+
+OpenXchangeResource::~OpenXchangeResource()
+{
+}
+
+void OpenXchangeResource::cleanup()
+{
+    // be nice and remove cache file when resource is removed
+    QFile::remove(OXA::Users::self()->cacheFilePath());
+
+    QFile::remove(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QLatin1Char('/') + Settings::self()->config()->name());
+
+    ResourceBase::cleanup();
+}
+
+void OpenXchangeResource::aboutToQuit()
+{
+}
+
+void OpenXchangeResource::configure(WId windowId)
+{
+    const bool useIncrementalUpdates = Settings::self()->useIncrementalUpdates();
+
+    ConfigDialog dlg(windowId);
+    dlg.setWindowIcon(QIcon::fromTheme(QStringLiteral("ox")));
+    if (dlg.exec()) {   //krazy:exclude=crashy
+
+        // if the user has changed the incremental update settings we have to do
+        // some additional initialization work
+        if (useIncrementalUpdates != Settings::self()->useIncrementalUpdates()) {
+            Settings::self()->setFoldersLastSync(0);
+            Settings::self()->setObjectsLastSync(QString());
+        }
+
+        Settings::self()->save();
+
+        clearCache();
+
+        QUrl baseUrl = QUrl::fromLocalFile(Settings::self()->baseUrl());
+        baseUrl.setUserName(Settings::self()->username());
+        baseUrl.setPassword(Settings::self()->password());
+        OXA::DavManager::self()->setBaseUrl(baseUrl);
+
+        // To find out the correct ACLs we need the uid of the user that
+        // logs in. For loading events and tasks we need a complete mapping of
+        // user id to name as well, so the mapping must be loaded as well.
+        // Both is done by UpdateUsersJob, so trigger it here before we continue
+        // with synchronization in onUpdateUsersJobFinished.
+        OXA::UpdateUsersJob *job = new OXA::UpdateUsersJob(this);
+        connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onUpdateUsersJobFinished);
+        job->start();
+    } else {
+        Q_EMIT configurationDialogRejected();
+    }
+}
+
+void OpenXchangeResource::retrieveCollections()
+{
+    //qDebug("tokoe: retrieve collections called");
+    if (Settings::self()->useIncrementalUpdates()) {
+        //qDebug( "lastSync=%llu", Settings::self()->foldersLastSync() );
+        OXA::FoldersRequestDeltaJob *job = new OXA::FoldersRequestDeltaJob(Settings::self()->foldersLastSync(), this);
+        connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onFoldersRequestDeltaJobFinished);
+        job->start();
+    } else {
+        OXA::FoldersRequestJob *job = new OXA::FoldersRequestJob(0, OXA::FoldersRequestJob::Modified, this);
+        connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onFoldersRequestJobFinished);
+        job->start();
+    }
+}
+
+void OpenXchangeResource::retrieveItems(const Akonadi::Collection &collection)
+{
+    //qDebug( "tokoe: retrieveItems on %s called", qPrintable( collection.name() ) );
+    const RemoteInformation remoteInformation = RemoteInformation::load(collection);
+
+    OXA::Folder folder;
+    folder.setObjectId(remoteInformation.objectId());
+    folder.setModule(remoteInformation.module());
+
+    if (Settings::self()->useIncrementalUpdates()) {
+        ObjectsLastSync lastSyncInfo;
+        //qDebug( "lastSync=%llu", lastSyncInfo.lastSync( collection.id() ) );
+        OXA::ObjectsRequestDeltaJob *job = new OXA::ObjectsRequestDeltaJob(folder, lastSyncInfo.lastSync(collection.id()), this);
+        job->setProperty("collection", QVariant::fromValue(collection));
+        connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onObjectsRequestDeltaJobFinished);
+        job->start();
+    } else {
+        OXA::ObjectsRequestJob *job = new OXA::ObjectsRequestJob(folder, 0, OXA::ObjectsRequestJob::Modified, this);
+        connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onObjectsRequestJobFinished);
+        job->start();
+    }
+}
+
+bool OpenXchangeResource::retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &)
+{
+    //qDebug( "tokoe: retrieveItem %lld called", item.id() );
+    const RemoteInformation remoteInformation = RemoteInformation::load(item);
+
+    OXA::Object object;
+    object.setObjectId(remoteInformation.objectId());
+    object.setModule(remoteInformation.module());
+
+    OXA::ObjectRequestJob *job = new OXA::ObjectRequestJob(object, this);
+    job->setProperty("item", QVariant::fromValue(item));
+    connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onObjectRequestJobFinished);
+    job->start();
+
+    return true;
+}
+
+void OpenXchangeResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection)
+{
+    const RemoteInformation remoteInformation = RemoteInformation::load(collection);
+
+    OXA::Object object;
+    object.setFolderId(remoteInformation.objectId());
+    object.setModule(remoteInformation.module());
+
+    if (item.hasPayload<KContacts::Addressee>()) {
+        object.setContact(item.payload<KContacts::Addressee>());
+    } else if (item.hasPayload<KContacts::ContactGroup>()) {
+        object.setContactGroup(item.payload<KContacts::ContactGroup>());
+    } else if (item.hasPayload<KCalCore::Event::Ptr>()) {
+        object.setEvent(item.payload<KCalCore::Incidence::Ptr>());
+    } else if (item.hasPayload<KCalCore::Todo::Ptr>()) {
+        object.setTask(item.payload<KCalCore::Incidence::Ptr>());
+    } else {
+        Q_ASSERT(false);
+    }
+
+    OXA::ObjectCreateJob *job = new OXA::ObjectCreateJob(object, this);
+    job->setProperty("item", QVariant::fromValue(item));
+    connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onObjectCreateJobFinished);
+    job->start();
+}
+
+void OpenXchangeResource::itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &)
+{
+    const RemoteInformation remoteInformation = RemoteInformation::load(item);
+    const RemoteInformation parentRemoteInformation = RemoteInformation::load(item.parentCollection());
+
+    OXA::Object object;
+    object.setObjectId(remoteInformation.objectId());
+    object.setModule(remoteInformation.module());
+    object.setFolderId(parentRemoteInformation.objectId());
+    object.setLastModified(remoteInformation.lastModified());
+
+    if (item.hasPayload<KContacts::Addressee>()) {
+        object.setContact(item.payload<KContacts::Addressee>());
+    } else if (item.hasPayload<KContacts::ContactGroup>()) {
+        object.setContactGroup(item.payload<KContacts::ContactGroup>());
+    } else if (item.hasPayload<KCalCore::Event::Ptr>()) {
+        object.setEvent(item.payload<KCalCore::Incidence::Ptr>());
+    } else if (item.hasPayload<KCalCore::Todo::Ptr>()) {
+        object.setTask(item.payload<KCalCore::Incidence::Ptr>());
+    } else {
+        Q_ASSERT(false);
+    }
+
+    OXA::ObjectModifyJob *job = new OXA::ObjectModifyJob(object, this);
+    job->setProperty("item", QVariant::fromValue(item));
+    connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onObjectModifyJobFinished);
+    job->start();
+}
+
+void OpenXchangeResource::itemRemoved(const Akonadi::Item &item)
+{
+    const RemoteInformation remoteInformation = RemoteInformation::load(item);
+    const RemoteInformation parentRemoteInformation = RemoteInformation::load(item.parentCollection());
+
+    OXA::Object object;
+    object.setObjectId(remoteInformation.objectId());
+    object.setFolderId(parentRemoteInformation.objectId());
+    object.setModule(remoteInformation.module());
+    object.setLastModified(remoteInformation.lastModified());
+
+    OXA::ObjectDeleteJob *job = new OXA::ObjectDeleteJob(object, this);
+    connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onObjectDeleteJobFinished);
+
+    job->start();
+}
+
+void OpenXchangeResource::itemMoved(const Akonadi::Item &item, const Akonadi::Collection &collectionSource,
+                                    const Akonadi::Collection &collectionDestination)
+{
+    const RemoteInformation remoteInformation = RemoteInformation::load(item);
+    const RemoteInformation parentRemoteInformation = RemoteInformation::load(collectionSource);
+    const RemoteInformation newParentRemoteInformation = RemoteInformation::load(collectionDestination);
+
+    OXA::Object object;
+    object.setObjectId(remoteInformation.objectId());
+    object.setModule(remoteInformation.module());
+    object.setFolderId(parentRemoteInformation.objectId());
+    object.setLastModified(remoteInformation.lastModified());
+
+    OXA::Folder destinationFolder;
+    destinationFolder.setObjectId(newParentRemoteInformation.objectId());
+
+    OXA::ObjectMoveJob *job = new OXA::ObjectMoveJob(object, destinationFolder, this);
+    job->setProperty("item", QVariant::fromValue(item));
+    connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onObjectMoveJobFinished);
+
+    job->start();
+}
+
+void OpenXchangeResource::collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent)
+{
+    const RemoteInformation parentRemoteInformation = RemoteInformation::load(parent);
+
+    OXA::Folder folder;
+    folder.setTitle(collection.name());
+    folder.setFolderId(parentRemoteInformation.objectId());
+    folder.setType(OXA::Folder::Private);
+
+    // the folder 'inherits' the module type of its parent collection
+    folder.setModule(parentRemoteInformation.module());
+
+    // fill permissions
+    OXA::Folder::Permissions permissions;
+    permissions.setFolderPermission(OXA::Folder::Permissions::CreateSubfolders);
+    permissions.setObjectReadPermission(OXA::Folder::Permissions::ReadOwnObjects);
+    permissions.setObjectWritePermission(OXA::Folder::Permissions::WriteOwnObjects);
+    permissions.setObjectDeletePermission(OXA::Folder::Permissions::DeleteOwnObjects);
+    permissions.setAdminFlag(true);
+
+    // assign permissions to user
+    OXA::Folder::UserPermissions userPermissions;
+    userPermissions.insert(OXA::Users::self()->currentUserId(), permissions);
+
+    // set user permissions of folder
+    folder.setUserPermissions(userPermissions);
+
+    OXA::FolderCreateJob *job = new OXA::FolderCreateJob(folder, this);
+    job->setProperty("collection", QVariant::fromValue(collection));
+    connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onFolderCreateJobFinished);
+    job->start();
+}
+
+void OpenXchangeResource::collectionChanged(const Akonadi::Collection &collection)
+{
+    const RemoteInformation remoteInformation = RemoteInformation::load(collection);
+
+    // do not try to change the standard collections
+    if (remoteInformation.objectId() >= 0 && remoteInformation.objectId() <= 4) {
+        changeCommitted(collection);
+        return;
+    }
+
+    const RemoteInformation parentRemoteInformation = RemoteInformation::load(collection.parentCollection());
+
+    OXA::Folder folder;
+    folder.setObjectId(remoteInformation.objectId());
+    folder.setFolderId(parentRemoteInformation.objectId());
+    folder.setTitle(collection.name());
+    folder.setLastModified(remoteInformation.lastModified());
+
+    OXA::FolderModifyJob *job = new OXA::FolderModifyJob(folder, this);
+    job->setProperty("collection", QVariant::fromValue(collection));
+    connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onFolderModifyJobFinished);
+}
+
+void OpenXchangeResource::collectionRemoved(const Akonadi::Collection &collection)
+{
+    const RemoteInformation remoteInformation = RemoteInformation::load(collection);
+
+    OXA::Folder folder;
+    folder.setObjectId(remoteInformation.objectId());
+    folder.setLastModified(remoteInformation.lastModified());
+
+    OXA::FolderDeleteJob *job = new OXA::FolderDeleteJob(folder, this);
+    connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onFolderDeleteJobFinished);
+
+    job->start();
+}
+
+void OpenXchangeResource::collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &collectionSource,
+        const Akonadi::Collection &collectionDestination)
+{
+    const RemoteInformation remoteInformation = RemoteInformation::load(collection);
+    const RemoteInformation parentRemoteInformation = RemoteInformation::load(collectionSource);
+    const RemoteInformation newParentRemoteInformation = RemoteInformation::load(collectionDestination);
+
+    OXA::Folder folder;
+    folder.setObjectId(remoteInformation.objectId());
+    folder.setFolderId(parentRemoteInformation.objectId());
+
+    OXA::Folder destinationFolder;
+    destinationFolder.setObjectId(newParentRemoteInformation.objectId());
+
+    OXA::FolderMoveJob *job = new OXA::FolderMoveJob(folder, destinationFolder, this);
+    job->setProperty("collection", QVariant::fromValue(collection));
+    connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onFolderMoveJobFinished);
+
+    job->start();
+}
+
+//// job result slots
+
+void OpenXchangeResource::onUpdateUsersJobFinished(KJob *job)
+{
+    if (job->error()) {
+        // This might be an indication that we can not connect to the server...
+        Q_EMIT status(Broken, i18n("Unable to connect to server"));
+        return;
+    }
+
+    if (Settings::self()->useIncrementalUpdates()) {
+        syncCollectionsRemoteIdCache();
+    }
+
+    // now we have all user information, so continue synchronization
+    synchronize();
+    Q_EMIT configurationDialogAccepted();
+}
+
+void OpenXchangeResource::onObjectsRequestJobFinished(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(job->errorText());
+        return;
+    }
+
+    OXA::ObjectsRequestJob *requestJob = qobject_cast<OXA::ObjectsRequestJob *>(job);
+    Q_ASSERT(requestJob);
+
+    Item::List items;
+
+    const OXA::Object::List objects = requestJob->objects();
+    foreach (const OXA::Object &object, objects) {
+        Item item;
+        switch (object.module()) {
+        case OXA::Folder::Contacts:
+            if (!object.contact().isEmpty()) {
+                item.setMimeType(KContacts::Addressee::mimeType());
+                item.setPayload<KContacts::Addressee>(object.contact());
+            } else {
+                item.setMimeType(KContacts::ContactGroup::mimeType());
+                item.setPayload<KContacts::ContactGroup>(object.contactGroup());
+            }
+            break;
+        case OXA::Folder::Calendar:
+            item.setMimeType(KCalCore::Event::eventMimeType());
+            item.setPayload<KCalCore::Incidence::Ptr>(object.event());
+            break;
+        case OXA::Folder::Tasks:
+            item.setMimeType(KCalCore::Todo::todoMimeType());
+            item.setPayload<KCalCore::Incidence::Ptr>(object.task());
+            break;
+        case OXA::Folder::Unbound:
+            Q_ASSERT(false);
+            break;
+        }
+        const RemoteInformation remoteInformation(object.objectId(), object.module(), object.lastModified());
+        remoteInformation.store(item);
+
+        items.append(item);
+    }
+
+    itemsRetrieved(items);
+}
+
+void OpenXchangeResource::onObjectsRequestDeltaJobFinished(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(job->errorText());
+        return;
+    }
+
+    OXA::ObjectsRequestDeltaJob *requestJob = qobject_cast<OXA::ObjectsRequestDeltaJob *>(job);
+    Q_ASSERT(requestJob);
+
+    const Akonadi::Collection collection = requestJob->property("collection").value<Collection>();
+
+    ObjectsLastSync lastSyncInfo;
+
+    qulonglong objectsLastSync = lastSyncInfo.lastSync(collection.id());
+
+    Item::List changedItems;
+
+    const OXA::Object::List modifiedObjects = requestJob->modifiedObjects();
+    foreach (const OXA::Object &object, modifiedObjects) {
+        Item item;
+        switch (object.module()) {
+        case OXA::Folder::Contacts:
+            if (!object.contact().isEmpty()) {
+                item.setMimeType(KContacts::Addressee::mimeType());
+                item.setPayload<KContacts::Addressee>(object.contact());
+            } else {
+                item.setMimeType(KContacts::ContactGroup::mimeType());
+                item.setPayload<KContacts::ContactGroup>(object.contactGroup());
+            }
+            break;
+        case OXA::Folder::Calendar:
+            item.setMimeType(KCalCore::Event::eventMimeType());
+            item.setPayload<KCalCore::Incidence::Ptr>(object.event());
+            break;
+        case OXA::Folder::Tasks:
+            item.setMimeType(KCalCore::Todo::todoMimeType());
+            item.setPayload<KCalCore::Incidence::Ptr>(object.task());
+            break;
+        case OXA::Folder::Unbound:
+            Q_ASSERT(false);
+            break;
+        }
+        const RemoteInformation remoteInformation(object.objectId(), object.module(), object.lastModified());
+        remoteInformation.store(item);
+
+        // the value of objectsLastSync is determined by the maximum last modified value
+        // of the added or changed objects
+        objectsLastSync = qMax(objectsLastSync, object.lastModified().toULongLong());
+
+        changedItems.append(item);
+    }
+
+    Item::List removedItems;
+
+    const OXA::Object::List deletedObjects = requestJob->deletedObjects();
+    removedItems.reserve(deletedObjects.count());
+    foreach (const OXA::Object &object, deletedObjects) {
+        Item item;
+
+        const RemoteInformation remoteInformation(object.objectId(), object.module(), object.lastModified());
+        remoteInformation.store(item);
+
+        removedItems.append(item);
+    }
+
+    if (objectsLastSync != lastSyncInfo.lastSync(collection.id())) {
+        // according to the OX developers we should subtract one millisecond from the
+        // maximum last modified value to cover multiple changes that might have been
+        // done in the same millisecond to the data on the server
+        lastSyncInfo.setLastSync(collection.id(), objectsLastSync - 1);
+        lastSyncInfo.save();
+    }
+
+    //qDebug( "changedObjects=%d removedObjects=%d", modifiedObjects.count(), deletedObjects.count() );
+    //qDebug( "changedItems=%d removedItems=%d", changedItems.count(), removedItems.count() );
+    itemsRetrievedIncremental(changedItems, removedItems);
+}
+
+void OpenXchangeResource::onObjectRequestJobFinished(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(job->errorText());
+        return;
+    }
+
+    OXA::ObjectRequestJob *requestJob = qobject_cast<OXA::ObjectRequestJob *>(job);
+    Q_ASSERT(requestJob);
+
+    const OXA::Object object = requestJob->object();
+
+    Item item = job->property("item").value<Item>();
+
+    const RemoteInformation remoteInformation(object.objectId(), object.module(), object.lastModified());
+    remoteInformation.store(item);
+
+    switch (object.module()) {
+    case OXA::Folder::Contacts:
+        if (!object.contact().isEmpty()) {
+            item.setMimeType(KContacts::Addressee::mimeType());
+            item.setPayload<KContacts::Addressee>(object.contact());
+        } else {
+            item.setMimeType(KContacts::ContactGroup::mimeType());
+            item.setPayload<KContacts::ContactGroup>(object.contactGroup());
+        }
+        break;
+    case OXA::Folder::Calendar:
+        item.setMimeType(KCalCore::Event::eventMimeType());
+        item.setPayload<KCalCore::Incidence::Ptr>(object.event());
+        break;
+    case OXA::Folder::Tasks:
+        item.setMimeType(KCalCore::Todo::todoMimeType());
+        item.setPayload<KCalCore::Incidence::Ptr>(object.task());
+        break;
+    case OXA::Folder::Unbound:
+        Q_ASSERT(false);
+        break;
+    }
+
+    itemRetrieved(item);
+}
+
+void OpenXchangeResource::onObjectCreateJobFinished(KJob *job)
+{
+    if (job->error()) {
+        QString errorText = job->errorText();
+        if (job->error() == KJob::UserDefinedError) {
+            switch (OXA::OXErrors::getEditErrorID(job->errorText())) {
+            case OXA::OXErrors::ConcurrentModification : errorText = i18n("The object was edited by another participant in the meantime. Please check."); break;
+            case OXA::OXErrors::ObjectNotFound : errorText = i18n("Object not found. Maybe it was deleted by another participant in the meantime."); break;
+            case OXA::OXErrors::NoPermissionForThisAction : errorText = i18n("You don't have the permission to perform this action on this object."); break;
+            case OXA::OXErrors::ConflictsDetected : errorText = i18n("A conflict detected. Please check if there are other objects in conflict with this one."); break;
+            case OXA::OXErrors::MissingMandatoryFields : errorText = i18n("A mandatory data field is missing. Please check. Otherwise contact your administrator."); break;
+            case OXA::OXErrors::AppointmentConflicts : errorText = i18n("An appointment conflict detected.\nPlease check if there are other appointments in conflict with this one."); break;
+            case OXA::OXErrors::InternalServerError : errorText = i18n("Internal server error. Please contact your administrator."); break;
+            case OXA::OXErrors::EditErrorUndefined :
+            default :;
+            }
+        }
+        cancelTask(errorText);
+        return;
+    }
+
+    OXA::ObjectCreateJob *createJob = qobject_cast<OXA::ObjectCreateJob *>(job);
+    Q_ASSERT(createJob);
+
+    const OXA::Object object = createJob->object();
+
+    Item item = job->property("item").value<Item>();
+
+    const RemoteInformation remoteInformation(object.objectId(), object.module(), object.lastModified());
+    remoteInformation.store(item);
+
+    changeCommitted(item);
+}
+
+void OpenXchangeResource::onObjectModifyJobFinished(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(job->errorText());
+        return;
+    }
+
+    OXA::ObjectModifyJob *modifyJob = qobject_cast<OXA::ObjectModifyJob *>(job);
+    Q_ASSERT(modifyJob);
+
+    const OXA::Object object = modifyJob->object();
+
+    Item item = job->property("item").value<Item>();
+
+    // update last_modified property
+    RemoteInformation remoteInformation = RemoteInformation::load(item);
+    remoteInformation.setLastModified(object.lastModified());
+    remoteInformation.store(item);
+
+    changeCommitted(item);
+}
+
+void OpenXchangeResource::onObjectMoveJobFinished(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(job->errorText());
+        return;
+    }
+
+    OXA::ObjectMoveJob *moveJob = qobject_cast<OXA::ObjectMoveJob *>(job);
+    Q_ASSERT(moveJob);
+
+    const OXA::Object object = moveJob->object();
+
+    Item item = job->property("item").value<Item>();
+
+    // update last_modified property
+    RemoteInformation remoteInformation = RemoteInformation::load(item);
+    remoteInformation.setLastModified(object.lastModified());
+    remoteInformation.store(item);
+
+    changeCommitted(item);
+}
+
+void OpenXchangeResource::onObjectDeleteJobFinished(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(job->errorText());
+        return;
+    }
+
+    changeProcessed();
+}
+
+static Collection folderToCollection(const OXA::Folder &folder, const Collection &parentCollection)
+{
+    Collection collection;
+
+    collection.setParentCollection(parentCollection);
+
+    const RemoteInformation remoteInformation(folder.objectId(), folder.module(), folder.lastModified());
+    remoteInformation.store(collection);
+
+    // set a unique name to make Akonadi happy
+    collection.setName(folder.title() + QLatin1Char('_') + QUuid::createUuid().toString());
+
+    EntityDisplayAttribute *attribute = collection.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
+    attribute->setDisplayName(folder.title());
+
+    QStringList mimeTypes;
+    mimeTypes.append(Collection::mimeType());
+    switch (folder.module()) {
+    case OXA::Folder::Calendar:
+        mimeTypes.append(KCalCore::Event::eventMimeType());
+        attribute->setIconName(QStringLiteral("view-calendar"));
+        break;
+    case OXA::Folder::Contacts:
+        mimeTypes.append(KContacts::Addressee::mimeType());
+        mimeTypes.append(KContacts::ContactGroup::mimeType());
+        attribute->setIconName(QStringLiteral("view-pim-contacts"));
+        break;
+    case OXA::Folder::Tasks:
+        mimeTypes.append(KCalCore::Todo::todoMimeType());
+        attribute->setIconName(QStringLiteral("view-pim-tasks"));
+        break;
+    case OXA::Folder::Unbound:
+        break;
+    }
+
+    collection.setContentMimeTypes(mimeTypes);
+    collection.setRights(folderPermissionsToCollectionRights(folder));
+
+    return collection;
+}
+
+void OpenXchangeResource::onFoldersRequestJobFinished(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(job->errorText());
+        return;
+    }
+
+    OXA::FoldersRequestJob *requestJob = qobject_cast<OXA::FoldersRequestJob *>(job);
+    Q_ASSERT(requestJob);
+
+    Collection::List collections;
+
+    // add the standard collections
+    collections << Akonadi::valuesToVector(mStandardCollectionsMap);
+
+    QMap<qlonglong, Collection> remoteIdMap(mStandardCollectionsMap);
+
+    // add the folders from the server
+    OXA::Folder::List folders = requestJob->folders();
+    while (!folders.isEmpty()) {
+        const OXA::Folder folder = folders.takeFirst();
+        if (remoteIdMap.contains(folder.folderId())) {
+            // we have the parent collection created already
+            const Collection collection = folderToCollection(folder, remoteIdMap.value(folder.folderId()));
+            remoteIdMap.insert(folder.objectId(), collection);
+            collections.append(collection);
+        } else {
+            // we have to wait until the parent folder has been created
+            folders.append(folder);
+            qDebug() << "Error: parent folder id" << folder.folderId() << "of folder" << folder.title() << "is unknown";
+        }
+    }
+
+    collectionsRetrieved(collections);
+}
+
+void OpenXchangeResource::onFoldersRequestDeltaJobFinished(KJob *job)
+{
+    //qDebug( "onFoldersRequestDeltaJobFinished mCollectionsMap.count() = %d", mCollectionsMap.count() );
+
+    if (job->error()) {
+        cancelTask(job->errorText());
+        return;
+    }
+
+    OXA::FoldersRequestDeltaJob *requestJob = qobject_cast<OXA::FoldersRequestDeltaJob *>(job);
+    Q_ASSERT(requestJob);
+
+    Collection::List changedCollections;
+
+    // add the standard collections
+    changedCollections << Akonadi::valuesToVector(mStandardCollectionsMap);
+
+    qulonglong foldersLastSync = Settings::self()->foldersLastSync();
+
+    // add the new or modified folders from the server
+    OXA::Folder::List modifiedFolders = requestJob->modifiedFolders();
+    while (!modifiedFolders.isEmpty()) {
+        const OXA::Folder folder = modifiedFolders.takeFirst();
+        if (mCollectionsMap.contains(folder.folderId())) {
+            // we have the parent collection created already
+            const Collection collection = folderToCollection(folder, mCollectionsMap.value(folder.folderId()));
+            mCollectionsMap.insert(folder.objectId(), collection);
+            changedCollections.append(collection);
+
+            // the value of foldersLastSync is determined by the maximum last modified value
+            // of the added or changed folders
+            foldersLastSync = qMax(foldersLastSync, folder.lastModified().toULongLong());
+
+        } else {
+            // we have to wait until the parent folder has been created
+            modifiedFolders.append(folder);
+            qDebug() << "Error: parent folder id" << folder.folderId() << "of folder" << folder.title() << "is unknown";
+        }
+    }
+
+    Collection::List removedCollections;
+
+    // add the deleted folders from the server
+    OXA::Folder::List deletedFolders = requestJob->deletedFolders();
+    removedCollections.reserve(deletedFolders.count());
+    foreach (const OXA::Folder &folder, deletedFolders) {
+        Collection collection;
+        collection.setRemoteId(QString::number(folder.objectId()));
+
+        removedCollections.append(collection);
+    }
+
+    if (foldersLastSync != Settings::self()->foldersLastSync()) {
+        // according to the OX developers we should subtract one millisecond from the
+        // maximum last modified value to cover multiple changes that might have been
+        // done in the same millisecond to the data on the server
+        Settings::self()->setFoldersLastSync(foldersLastSync - 1);
+        Settings::self()->save();
+    }
+
+    //qDebug( "changedFolders=%d removedFolders=%d", modifiedFolders.count(), deletedFolders.count() );
+    //qDebug( "changedCollections=%d removedCollections=%d", changedCollections.count(), removedCollections.count() );
+    collectionsRetrievedIncremental(changedCollections, removedCollections);
+}
+
+void OpenXchangeResource::onFolderCreateJobFinished(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(job->errorText());
+        return;
+    }
+
+    OXA::FolderCreateJob *createJob = qobject_cast<OXA::FolderCreateJob *>(job);
+    Q_ASSERT(createJob);
+
+    const OXA::Folder folder = createJob->folder();
+
+    Collection collection = job->property("collection").value<Collection>();
+
+    const RemoteInformation remoteInformation(folder.objectId(), folder.module(), folder.lastModified());
+    remoteInformation.store(collection);
+
+    // set matching icon
+    EntityDisplayAttribute *attribute = collection.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
+    switch (folder.module()) {
+    case OXA::Folder::Calendar:
+        attribute->setIconName(QStringLiteral("view-calendar"));
+        break;
+    case OXA::Folder::Contacts:
+        attribute->setIconName(QStringLiteral("view-pim-contacts"));
+        break;
+    case OXA::Folder::Tasks:
+        attribute->setIconName(QStringLiteral("view-pim-tasks"));
+        break;
+    case OXA::Folder::Unbound:
+        break;
+    }
+
+    changeCommitted(collection);
+}
+
+void OpenXchangeResource::onFolderModifyJobFinished(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(job->errorText());
+        return;
+    }
+
+    OXA::FolderModifyJob *modifyJob = qobject_cast<OXA::FolderModifyJob *>(job);
+    Q_ASSERT(modifyJob);
+
+    const OXA::Folder folder = modifyJob->folder();
+
+    Collection collection = job->property("collection").value<Collection>();
+
+    // update last_modified property
+    RemoteInformation remoteInformation = RemoteInformation::load(collection);
+    remoteInformation.setLastModified(folder.lastModified());
+    remoteInformation.store(collection);
+
+    changeCommitted(collection);
+}
+
+void OpenXchangeResource::onFolderMoveJobFinished(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(job->errorText());
+        return;
+    }
+
+    OXA::FolderMoveJob *moveJob = qobject_cast<OXA::FolderMoveJob *>(job);
+    Q_ASSERT(moveJob);
+
+    const OXA::Folder folder = moveJob->folder();
+
+    Collection collection = job->property("collection").value<Collection>();
+
+    // update last_modified property
+    RemoteInformation remoteInformation = RemoteInformation::load(collection);
+    remoteInformation.setLastModified(folder.lastModified());
+    remoteInformation.store(collection);
+
+    changeCommitted(collection);
+}
+
+void OpenXchangeResource::onFolderDeleteJobFinished(KJob *job)
+{
+    if (job->error()) {
+        cancelTask(job->errorText());
+        return;
+    }
+
+    changeProcessed();
+}
+
+/**
+ * For incremental updates we need a mapping between the folder id
+ * and the collection object for all collections of this resource,
+ * so that we can find out the right parent collection in
+ * onFoldersRequestDeltaJob().
+ *
+ * Therefor we trigger this method when the resource is started and
+ * configured to use incremental sync.
+ */
+void OpenXchangeResource::syncCollectionsRemoteIdCache()
+{
+    mCollectionsMap.clear();
+
+    // copy the standard collections
+    mCollectionsMap = mStandardCollectionsMap;
+
+    CollectionFetchJob *job = new CollectionFetchJob(mResourceCollection, CollectionFetchJob::Recursive, this);
+    connect(job, &OXA::UpdateUsersJob::result, this, &OpenXchangeResource::onFetchResourceCollectionsFinished);
+}
+
+void OpenXchangeResource::onFetchResourceCollectionsFinished(KJob *job)
+{
+    if (job->error()) {
+        qDebug() << "Error: Unable to fetch resource collections:" << job->errorText();
+        return;
+    }
+
+    const CollectionFetchJob *fetchJob = qobject_cast<CollectionFetchJob *>(job);
+
+    // copy the remaining collections of the resource
+    const Collection::List collections = fetchJob->collections();
+    foreach (const Collection &collection, collections) {
+        mCollectionsMap.insert(collection.remoteId().toULongLong(), collection);
+    }
+}
+
+AKONADI_RESOURCE_MAIN(OpenXchangeResource)
+
diff --git a/resources/openxchange/openxchangeresource.desktop b/resources/openxchange/openxchangeresource.desktop
new file mode 100644 (file)
index 0000000..ba64086
--- /dev/null
@@ -0,0 +1,90 @@
+[Desktop Entry]
+Name=Open-Xchange Groupware Server
+Name[bg]=Сървър Open-Xchange Groupware
+Name[bs]=Open-Xchange grupni server
+Name[ca]=Servidor de treball en grup Open-Xchange
+Name[ca@valencia]=Servidor de treball en grup Open-Xchange
+Name[cs]=Server Open-Xchange Groupware
+Name[da]=Open-Xchange groupware-server
+Name[de]=Open-Xchange-Groupware-Server
+Name[el]=Εξυπηρετητής Groupware OpenXchange
+Name[en_GB]=Open-Xchange Groupware Server
+Name[es]=Servidor de colaboración Open-Xchange
+Name[et]=Open-Xchange'i grupitöö server
+Name[fi]=Open-Xchange-työryhmäpalvelin
+Name[fr]=Serveur de logiciels de collaboration Open-Xchange
+Name[ga]=Freastalaí Groupware Open-Xchange
+Name[gl]=Servidor de traballo en grupo OpenXchange
+Name[hu]=Open-Xchange csoportmunka-kiszolgáló
+Name[ia]=Servitor de Open-Xchange Groupware
+Name[it]=Server di groupware Open-Xchange
+Name[ja]=Open-Xchange グループウェアサーバ
+Name[kk]=Open-Xchange топтық іс сервері
+Name[km]=ម៉ាស៊ីន​បម្រើ Open-Xchange Groupware
+Name[ko]=Open-Xchange 그룹웨어 서버
+Name[lt]=Open-Xchange grupinio darbo serveris
+Name[lv]=Open-Xchange grupprogrammatūras serveris
+Name[nb]=OpenXchange-tjener
+Name[nds]=OpenXchange-Arbeitkoppelserver
+Name[nl]=Open-Xchange-groupware-server
+Name[nn]=Open-Xchange-tenar
+Name[pa]=Open-Xchange ਗਰੁੱਪਵੇਅਰ ਸਰਵਰ
+Name[pl]=Serwer Open-Xchange Groupware
+Name[pt]=Servidor de 'Groupware' Open-Xchange
+Name[pt_BR]=Servidor groupware Open-Xchange
+Name[ru]=Сервер совместной работы Open-Xchange
+Name[sk]=Groupware Server Open-Xchange
+Name[sl]=Strežnik za skupinsko delo Open-Xchange
+Name[sr]=Опен‑ексчејнџов групверски сервер
+Name[sr@ijekavian]=Опен‑ексчејнџов групверски сервер
+Name[sr@ijekavianlatin]=Open‑Xchangeov grupverski server
+Name[sr@latin]=Open‑Xchangeov grupverski server
+Name[sv]=Open-xchange grupprogramserver
+Name[tr]=Open-Xchange Groupware Sunucusu
+Name[uk]=Сервер групової роботи Open-Xchange
+Name[x-test]=xxOpen-Xchange Groupware Serverxx
+Name[zh_CN]=Open-Xchange 群件服务器
+Name[zh_TW]=Open-Xchange 群組伺服器
+Comment="Provides access to the appointments, tasks, and contacts of an Open-Xchange groupware server."
+Comment[bs]="Obezbjeđuje pristup zakazanim obavezama, zadacima  i kontaktima za Open-Xchange grupni server."
+Comment[ca]=Proporciona accés a les cites, tasques i contactes emmagatzemats en un servidor de treball en grup Open-Xchange.
+Comment[ca@valencia]=Proporciona accés a les cites, tasques i contactes emmagatzemats en un servidor de treball en grup Open-Xchange.
+Comment[da]="Giver adgang til aftaler, opgaver og kontakter på en Open-Xchange groupware-server."
+Comment[de]="Ermöglicht den Zugriff auf Termine, Aufgaben und Kontakte, die auf einem Open-Xchange-Server gespeichert sind."
+Comment[el]="Παρέχει πρόσβαση σε ραντεβού, εργασίες και επαφές ενός εξυπηρετητή groupware Open-Xchange"
+Comment[en_GB]="Provides access to the appointments, tasks, and contacts of an Open-Xchange groupware server."
+Comment[es]=«Proporciona acceso a las citas, tareas y contactos almacenados en un servidor de colaboración Open-Xchange».
+Comment[et]="Võimaldab kasutada Open-Xchange'i grupitöö serveri kohtumisi, ülesandeid ja kontakte."
+Comment[fi]="Tarjoaa pääsyn Open-Xchange-työryhmäpalvelimen tapaamis-, tehtävä- ja yhteystietoihin."
+Comment[fr]=« Fournit l'accès aux contacts, aux rendez-vous et aux tâches stockés sur un serveur Open-Xchange de logiciels de collaboration. »
+Comment[gl]=«Fornece acceso ás citas, tarefas e contactos almacenados nun servidor de traballo en grupo Open-Xchange.»
+Comment[hu]=„Hozzáférést biztosít egy Open-Xchange csoportmunka kiszolgálón tárolt találkozókhoz, feladatokhoz és névjegyekhez.”
+Comment[it]="Consente l'accesso ad appuntamenti, attività e contatti di un server di groupware Open-Xchange."
+Comment[ko]="Open-Xchange 그룹웨어 서버의 약속, 할 일, 연락처 정보를 가져옵니다."
+Comment[lt]=„Suteikia prieigą prie susitikimų, užduočių ir kontaktų Open-Xchange serveryje grupinio darbo serveryje.“
+Comment[nb]=«Gir tilgang til avtaler, oppgaver og kontakter lagret på en Open-Xchange gruppevare-tjener.»
+Comment[nds]="Stellt Togriep op Terminen, Opgaven un Kontakten op en Open-Xchange-Arbeitkoppelserver praat."
+Comment[nl]="Geeft toegang tot afspraken, taken en contactpersonen op een Open-Xchange-groupware-server."
+Comment[pl]="Zapewnia dostęp do spotkań, zadań i kontaktów serwera groupware Open-Xchange."
+Comment[pt]=Oferece o acesso aos compromissos, tarefas e contactos guardados num servidor de 'groupware' Open-Xchange.
+Comment[pt_BR]="Fornece acesso aos compromissos, tarefas e contatos de um servidor groupware Open-Xchange."
+Comment[ru]="Доступ к встречам, задачам и контактам на сервере совместной работы Open-Xchange"
+Comment[sk]="Poskytuje prístup k schôdzkam, úlohám a kontaktom groupware servera Open-Xchange."
+Comment[sl]=»Omogoča dostop do sestankov, opravil in stikov, shranjenih na strežniku za skupinsko delo Open-Xchange.«
+Comment[sr]="Омогућава приступ састанцима, пословима и контактима са Опен‑ексчејнџовог групверског сервера."
+Comment[sr@ijekavian]="Омогућава приступ састанцима, пословима и контактима са Опен‑ексчејнџовог групверског сервера."
+Comment[sr@ijekavianlatin]="Omogućava pristup sastancima, poslovima i kontaktima sa Open‑Xchangeovog grupverskog servera."
+Comment[sr@latin]="Omogućava pristup sastancima, poslovima i kontaktima sa Open‑Xchangeovog grupverskog servera."
+Comment[sv]="Ger tillgång till möten, uppgifter och kontakter lagrade på en Open-Xchange grupprogramserver."
+Comment[tr]="Open-Xchange groupware sunucusundaki toplantı, yapılacak iş ve kişilere erişimi sağlar."
+Comment[uk]="Надає доступ до записів зустрічей, завдань і контактів, які зберігаються на сервері групової роботи Open-Xchange."
+Comment[x-test]=xx"Provides access to the appointments, tasks, and contacts of an Open-Xchange groupware server."xx
+Comment[zh_CN]=“提供对存储在 Open-Xchange 服务器上的约会、任务和联系人的访问支持。”
+Comment[zh_TW]=「提供存取儲存在 Open-Xchange 群組伺服器上的約會、工作與聯絡人的功能。」
+Type=AkonadiResource
+Exec=akonadi_openxchange_resource
+Icon=ox
+
+X-Akonadi-MimeTypes=text/directory,text/calendar,application/x-vnd.kde.contactgroup,application/x-vnd.akonadi.calendar.event,application/x-vnd.akonadi.calendar.todo,application/x-vnd.akonadi.calendar.journal,application/x-vnd.akonadi.calendar.freebusy
+X-Akonadi-Capabilities=Resource
+X-Akonadi-Identifier=akonadi_openxchange_resource
diff --git a/resources/openxchange/openxchangeresource.h b/resources/openxchange/openxchangeresource.h
new file mode 100644 (file)
index 0000000..a25d44f
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OPENXCHANGERESOURCE_H
+#define OPENXCHANGERESOURCE_H
+
+#include <resourcebase.h>
+
+class OpenXchangeResource : public Akonadi::ResourceBase, public Akonadi::AgentBase::ObserverV2
+{
+    Q_OBJECT
+
+public:
+    explicit OpenXchangeResource(const QString &id);
+    ~OpenXchangeResource();
+
+    void cleanup() Q_DECL_OVERRIDE;
+
+public Q_SLOTS:
+    void configure(WId windowId) Q_DECL_OVERRIDE;
+    void aboutToQuit() Q_DECL_OVERRIDE;
+
+protected Q_SLOTS:
+    void retrieveCollections() Q_DECL_OVERRIDE;
+    void retrieveItems(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    bool retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+
+protected:
+    void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    void itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+    void itemRemoved(const Akonadi::Item &item) Q_DECL_OVERRIDE;
+    virtual void itemMoved(const Akonadi::Item &item, const Akonadi::Collection &collectionSource,
+                           const Akonadi::Collection &collectionDestination) Q_DECL_OVERRIDE;
+
+    void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) Q_DECL_OVERRIDE;
+    void collectionChanged(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    // do not hide the other variant, use implementation from base class
+    // which just forwards to the one above
+    using Akonadi::AgentBase::ObserverV2::collectionChanged;
+    void collectionRemoved(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    virtual void collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &collectionSource,
+                                 const Akonadi::Collection &collectionDestination) Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void onUpdateUsersJobFinished(KJob *);
+    void onFoldersRequestJobFinished(KJob *);
+    void onFoldersRequestDeltaJobFinished(KJob *);
+    void onFolderCreateJobFinished(KJob *);
+    void onFolderModifyJobFinished(KJob *);
+    void onFolderMoveJobFinished(KJob *);
+    void onFolderDeleteJobFinished(KJob *);
+
+    void onObjectsRequestJobFinished(KJob *);
+    void onObjectsRequestDeltaJobFinished(KJob *);
+    void onObjectRequestJobFinished(KJob *);
+    void onObjectCreateJobFinished(KJob *);
+    void onObjectModifyJobFinished(KJob *);
+    void onObjectMoveJobFinished(KJob *);
+    void onObjectDeleteJobFinished(KJob *);
+
+    void onFetchResourceCollectionsFinished(KJob *);
+
+private:
+    void syncCollectionsRemoteIdCache();
+    QMap<qlonglong, Akonadi::Collection> mCollectionsMap;
+
+    Akonadi::Collection mResourceCollection;
+    QMap<qlonglong, Akonadi::Collection> mStandardCollectionsMap;
+};
+
+#endif
diff --git a/resources/openxchange/openxchangeresource.kcfg b/resources/openxchange/openxchangeresource.kcfg
new file mode 100644 (file)
index 0000000..66a114d
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:kcfg="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
+      http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
+  <kcfgfile/>
+  <group name="General">
+    <entry name="BaseUrl" type="String">
+      <label>Server URL</label>
+      <tooltip>The URL of the Open-Xchange server, should be something like https://myserver.org/</tooltip>
+      <whatsthis>The URL of the Open-Xchange server, should be something like https://myserver.org/</whatsthis>
+    </entry>
+    <entry name="Username" type="String">
+      <label context="the username to login on server">Username</label>
+      <tooltip>The username that is used to log into the Open-Xchange server</tooltip>
+      <whatsthis>The username that is used to log into the Open-Xchange server</whatsthis>
+    </entry>
+    <entry name="Password" type="Password">
+      <label>Password</label>
+      <tooltip>The password that is used to log into the Open-Xchange server</tooltip>
+      <whatsthis>The password that is used to log into the Open-Xchange server</whatsthis>
+    </entry>
+    <entry name="UseIncrementalUpdates" type="Bool">
+      <default>true</default>
+      <label>Use incremental updates</label>
+      <tooltip>Use incremental updates instead of reloading all data from the server each time</tooltip>
+      <whatsthis>Use incremental updates instead of reloading all data from the server each time</whatsthis>
+    </entry>
+    <entry name="FoldersLastSync" type="ULongLong">
+      <default>0</default>
+    </entry>
+    <entry name="ObjectsLastSync" type="String"/>
+  </group>
+</kcfg>
diff --git a/resources/openxchange/oxa/connectiontestjob.cpp b/resources/openxchange/oxa/connectiontestjob.cpp
new file mode 100644 (file)
index 0000000..80f5abf
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "connectiontestjob.h"
+
+#include <kio/job.h>
+#include <QUrl>
+
+using namespace OXA;
+
+ConnectionTestJob::ConnectionTestJob(const QString &url, const QString &user, const QString &password, QObject *parent)
+    : KJob(parent), mUrl(url), mUser(user), mPassword(password)
+{
+}
+
+void ConnectionTestJob::start()
+{
+    const QUrl url(mUrl + QStringLiteral("/ajax/login?action=login&name=%1&password=%2").arg(mUser).arg(mPassword));
+
+    KJob *job = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo);
+    connect(job, &KJob::result, this, &ConnectionTestJob::httpJobFinished);
+}
+
+void ConnectionTestJob::httpJobFinished(KJob *job)
+{
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+        emitResult();
+        return;
+    }
+
+    KIO::StoredTransferJob *transferJob = qobject_cast<KIO::StoredTransferJob *>(job);
+    Q_ASSERT(transferJob);
+
+    const QString data = QString::fromUtf8(transferJob->data());
+
+    // on success data contains something like: {"session":"e530578bca504aa89738fadde9e44b3d","random":"ac9090d2cc284fed926fa3c7e316c43b"}
+    // on failure data contains something like: {"category":1,"error_params":[],"error":"Invalid credentials.","error_id":"-1529642166-37","code":"LGI-0006"}
+    const int index = data.indexOf(QLatin1String("\"session\":\""));
+    if (index == -1) {   // error case
+        const int errorIndex = data.indexOf(QLatin1String("\"error\":\""));
+        const QString errorText = data.mid(errorIndex + 9, data.indexOf(QLatin1Char('"'), errorIndex + 10) - errorIndex - 9);
+
+        setError(UserDefinedError);
+        setErrorText(errorText);
+        emitResult();
+        return;
+    } else { // success case
+        const QString sessionId = data.mid(index + 11, 33);   // I assume here the session id is always 32  characters long :}
+
+        // logout correctly...
+        const QUrl url(mUrl + QStringLiteral("/ajax/login?action=logout&session=%1").arg(sessionId));
+        KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo);
+    }
+
+    emitResult();
+}
+
diff --git a/resources/openxchange/oxa/connectiontestjob.h b/resources/openxchange/oxa/connectiontestjob.h
new file mode 100644 (file)
index 0000000..295835a
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_CONNECTIONTESTJOB_H
+#define OXA_CONNECTIONTESTJOB_H
+
+#include <kjob.h>
+
+namespace OXA
+{
+
+class ConnectionTestJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    ConnectionTestJob(const QString &url, const QString &user, const QString &password, QObject *parent = Q_NULLPTR);
+
+    void start() Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void httpJobFinished(KJob *);
+
+private:
+    QString mUrl;
+    QString mUser;
+    QString mPassword;
+};
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/contactutils.cpp b/resources/openxchange/oxa/contactutils.cpp
new file mode 100644 (file)
index 0000000..51a77ec
--- /dev/null
@@ -0,0 +1,435 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "contactutils.h"
+
+#include "davutils.h"
+#include "oxutils.h"
+#include "users.h"
+
+#include <Akonadi/Contact/ContactGroupExpandJob>
+
+#include <QtCore/QBuffer>
+#include <QtXml/QDomElement>
+
+using namespace OXA;
+
+void OXA::ContactUtils::parseContact(const QDomElement &propElement, Object &object)
+{
+    bool isDistributionList = false;
+    QDomElement distributionListElement = propElement.firstChildElement(QStringLiteral("distributionlist_flag"));
+    if (!distributionListElement.isNull()) {
+        if (OXUtils::readBoolean(distributionListElement.text()) == true) {
+            isDistributionList = true;
+        }
+    }
+
+    if (isDistributionList) {
+        KContacts::ContactGroup contactGroup;
+
+        QDomElement element = propElement.firstChildElement();
+        while (!element.isNull()) {
+            const QString tagName = element.tagName();
+            const QString text = OXUtils::readString(element.text());
+
+            if (tagName == QLatin1String("displayname")) {
+                contactGroup.setName(text);
+            } else if (tagName == QLatin1String("distributionlist")) {
+                QDomElement emailElement = element.firstChildElement();
+                while (!emailElement.isNull()) {
+                    const QString tagName = emailElement.tagName();
+                    const QString text = OXUtils::readString(emailElement.text());
+
+                    if (tagName == QLatin1String("email")) {
+                        const int emailField = OXUtils::readNumber(emailElement.attribute(QStringLiteral("emailfield")));
+                        if (emailField == 0) {   // internal data
+                            KContacts::ContactGroup::Data data;
+                            data.setName(OXUtils::readString(emailElement.attribute(QStringLiteral("displayname"))));
+                            data.setEmail(text);
+
+                            contactGroup.append(data);
+                        } else { // external reference
+                            // we convert them to internal data, seems like a more stable approach
+                            KContacts::ContactGroup::Data data;
+                            const qlonglong uid = OXUtils::readNumber(emailElement.attribute(QStringLiteral("id")));
+
+                            const User user = Users::self()->lookupUid(uid);
+                            if (user.isValid()) {
+                                data.setName(user.name());
+                                data.setEmail(user.email());
+                            } else {
+                                // fallback: use the data from the element
+                                data.setName(OXUtils::readString(emailElement.attribute(QStringLiteral("displayname"))));
+                                data.setEmail(text);
+                            }
+
+                            contactGroup.append(data);
+                        }
+                    }
+
+                    emailElement = emailElement.nextSiblingElement();
+                }
+            }
+            element = element.nextSiblingElement();
+        }
+
+        object.setContactGroup(contactGroup);
+    } else {
+        KContacts::Addressee contact;
+        KContacts::Address homeAddress(KContacts::Address::Home);
+        KContacts::Address workAddress(KContacts::Address::Work);
+        KContacts::Address otherAddress(KContacts::Address::Dom);
+
+        QDomElement element = propElement.firstChildElement();
+        while (!element.isNull()) {
+            const QString tagName = element.tagName();
+            const QString text = OXUtils::readString(element.text());
+
+            // name
+            if (tagName == QLatin1String("title")) {
+                contact.setTitle(text);
+            } else if (tagName == QLatin1String("first_name")) {
+                contact.setGivenName(text);
+            } else if (tagName == QLatin1String("second_name")) {
+                contact.setAdditionalName(text);
+            } else if (tagName == QLatin1String("last_name")) {
+                contact.setFamilyName(text);
+            } else if (tagName == QLatin1String("suffix")) {
+                contact.setSuffix(text);
+            } else if (tagName == QLatin1String("displayname")) {
+                contact.setFormattedName(text);
+            } else if (tagName == QLatin1String("nickname")) {
+                contact.setNickName(text);
+                // dates
+            } else if (tagName == QLatin1String("birthday")) {
+                contact.setBirthday(OXUtils::readDateTime(element.text()));
+            } else if (tagName == QLatin1String("anniversary")) {
+                contact.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Anniversary"), OXUtils::readDateTime(element.text()).toString(Qt::ISODate));
+            } else if (tagName == QLatin1String("spouse_name")) {
+                contact.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-SpousesName"), text);
+                // addresses
+            } else if (tagName == QLatin1String("street")) {
+                homeAddress.setStreet(text);
+            } else if (tagName == QLatin1String("postal_code")) {
+                homeAddress.setPostalCode(text);
+            } else if (tagName == QLatin1String("city")) {
+                homeAddress.setLocality(text);
+            } else if (tagName == QLatin1String("country")) {
+                homeAddress.setCountry(text);
+            } else if (tagName == QLatin1String("state")) {
+                homeAddress.setRegion(text);
+            } else if (tagName == QLatin1String("business_street")) {
+                workAddress.setStreet(text);
+            } else if (tagName == QLatin1String("business_postal_code")) {
+                workAddress.setPostalCode(text);
+            } else if (tagName == QLatin1String("business_city")) {
+                workAddress.setLocality(text);
+            } else if (tagName == QLatin1String("business_country")) {
+                workAddress.setCountry(text);
+            } else if (tagName == QLatin1String("business_state")) {
+                workAddress.setRegion(text);
+            } else if (tagName == QLatin1String("second_street")) {
+                otherAddress.setStreet(text);
+            } else if (tagName == QLatin1String("second_postal_code")) {
+                otherAddress.setPostalCode(text);
+            } else if (tagName == QLatin1String("second_city")) {
+                otherAddress.setLocality(text);
+            } else if (tagName == QLatin1String("second_country")) {
+                otherAddress.setCountry(text);
+            } else if (tagName == QLatin1String("second_state")) {
+                otherAddress.setRegion(text);
+            } else if (tagName == QLatin1String("defaultaddress")) {
+                const int number = text.toInt();
+                if (number == 1) {
+                    workAddress.setType(workAddress.type() | KContacts::Address::Pref);
+                } else if (number == 2) {
+                    homeAddress.setType(homeAddress.type() | KContacts::Address::Pref);
+                } else if (number == 3) {
+                    otherAddress.setType(otherAddress.type() | KContacts::Address::Pref);
+                }
+                // further information
+            } else if (tagName == QLatin1String("note")) {
+                contact.setNote(text);
+            } else if (tagName == QLatin1String("url")) {
+                KContacts::ResourceLocatorUrl url;
+                url.setUrl(QUrl(text));
+                contact.setUrl(url);
+            } else if (tagName == QLatin1String("image1")) {
+                const QByteArray data = text.toUtf8();
+                contact.setPhoto(KContacts::Picture(QImage::fromData(QByteArray::fromBase64(data))));
+                // company information
+            } else if (tagName == QLatin1String("company")) {
+                contact.setOrganization(text);
+            } else if (tagName == QLatin1String("department")) {
+                contact.setDepartment(text);
+            } else if (tagName == QLatin1String("assistants_name")) {
+                contact.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-AssistantsName"), text);
+            } else if (tagName == QLatin1String("managers_name")) {
+                contact.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-ManagersName"), text);
+            } else if (tagName == QLatin1String("position")) {
+                contact.setRole(text);
+            } else if (tagName == QLatin1String("profession")) {
+                contact.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Profession"), text);
+            } else if (tagName == QLatin1String("room_number")) {
+                contact.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Office"), text);
+                // communication
+            } else if (tagName == QLatin1String("email1")) {
+                contact.insertEmail(text, true);
+            } else if (tagName == QLatin1String("email2") ||
+                       tagName == QLatin1String("email3")) {
+                contact.insertEmail(text);
+            } else if (tagName == QLatin1String("mobile1")) {
+                contact.insertPhoneNumber(KContacts::PhoneNumber(text, KContacts::PhoneNumber::Cell));
+            } else if (tagName == QLatin1String("instant_messenger")) {
+                contact.insertCustom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-IMAddress"), text);
+            } else if (tagName.startsWith(QLatin1String("phone_"))) {
+                KContacts::PhoneNumber number;
+                number.setNumber(text);
+                bool supportedType = false;
+
+                if (tagName.endsWith(QLatin1String("_business"))) {
+                    number.setType(KContacts::PhoneNumber::Work);
+                    supportedType = true;
+                } else if (tagName.endsWith(QLatin1String("_home"))) {
+                    number.setType(KContacts::PhoneNumber::Home);
+                    supportedType = true;
+                } else if (tagName.endsWith(QLatin1String("_other"))) {
+                    number.setType(KContacts::PhoneNumber::Voice);
+                    supportedType = true;
+                } else if (tagName.endsWith(QLatin1String("_car"))) {
+                    number.setType(KContacts::PhoneNumber::Car);
+                    supportedType = true;
+                }
+
+                if (supportedType) {
+                    contact.insertPhoneNumber(number);
+                }
+            } else if (tagName.startsWith(QLatin1String("fax_"))) {
+                KContacts::PhoneNumber number;
+                number.setNumber(text);
+                bool supportedType = false;
+
+                if (tagName.endsWith(QLatin1String("_business"))) {
+                    number.setType(KContacts::PhoneNumber::Fax | KContacts::PhoneNumber::Work);
+                    supportedType = true;
+                } else if (tagName.endsWith(QLatin1String("_home"))) {
+                    number.setType(KContacts::PhoneNumber::Fax | KContacts::PhoneNumber::Home);
+                    supportedType = true;
+                } else if (tagName.endsWith(QLatin1String("_other"))) {
+                    number.setType(KContacts::PhoneNumber::Fax | KContacts::PhoneNumber::Voice);
+                    supportedType = true;
+                }
+
+                if (supportedType) {
+                    contact.insertPhoneNumber(number);
+                }
+            } else if (tagName == QLatin1String("pager")) {
+                contact.insertPhoneNumber(KContacts::PhoneNumber(text, KContacts::PhoneNumber::Pager));
+            } else if (tagName == QLatin1String("categories")) {
+                contact.setCategories(text.split(QRegExp(QLatin1String(",\\s*"))));
+            }
+
+            element = element.nextSiblingElement();
+        }
+
+        if (!homeAddress.isEmpty()) {
+            contact.insertAddress(homeAddress);
+        }
+        if (!workAddress.isEmpty()) {
+            contact.insertAddress(workAddress);
+        }
+        if (!otherAddress.isEmpty()) {
+            contact.insertAddress(otherAddress);
+        }
+
+        object.setContact(contact);
+    }
+}
+
+void OXA::ContactUtils::addContactElements(QDomDocument &document, QDomElement &propElement, const Object &object, void *preloadedData)
+{
+    if (!object.contact().isEmpty()) {
+        // it is a contact payload
+
+        const KContacts::Addressee contact = object.contact();
+
+        // name
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("title"), OXUtils::writeString(contact.title()));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("first_name"), OXUtils::writeString(contact.givenName()));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("second_name"), OXUtils::writeString(contact.additionalName()));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("last_name"), OXUtils::writeString(contact.familyName()));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("suffix"), OXUtils::writeString(contact.suffix()));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("displayname"), OXUtils::writeString(contact.formattedName()));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("nickname"), OXUtils::writeString(contact.nickName()));
+
+        // dates
+        if (contact.birthday().date().isValid()) {
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("birthday"), OXUtils::writeDate(contact.birthday().date()));
+        } else {
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("birthday"));
+        }
+
+        // since QDateTime::to/fromString() doesn't carry timezone information, we have to fake it here
+        const QDate anniversary = QDate::fromString(contact.custom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Anniversary")), Qt::ISODate);
+        if (anniversary.isValid()) {
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("anniversary"), OXUtils::writeDate(anniversary));
+        } else {
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("anniversary"));
+        }
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("spouse_name"), OXUtils::writeString(contact.custom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-SpousesName"))));
+
+        // addresses
+        const KContacts::Address homeAddress = contact.address(KContacts::Address::Home);
+        if (!homeAddress.isEmpty()) {
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("street"), OXUtils::writeString(homeAddress.street()));
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("postal_code"), OXUtils::writeString(homeAddress.postalCode()));
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("city"), OXUtils::writeString(homeAddress.locality()));
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("state"), OXUtils::writeString(homeAddress.region()));
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("country"), OXUtils::writeString(homeAddress.country()));
+        }
+        const KContacts::Address workAddress = contact.address(KContacts::Address::Work);
+        if (!workAddress.isEmpty()) {
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("business_street"), OXUtils::writeString(workAddress.street()));
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("business_postal_code"), OXUtils::writeString(workAddress.postalCode()));
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("business_city"), OXUtils::writeString(workAddress.locality()));
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("business_state"), OXUtils::writeString(workAddress.region()));
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("business_country"), OXUtils::writeString(workAddress.country()));
+        }
+        const KContacts::Address otherAddress = contact.address(KContacts::Address::Dom);
+        if (!otherAddress.isEmpty()) {
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("second_street"), OXUtils::writeString(otherAddress.street()));
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("second_postal_code"), OXUtils::writeString(otherAddress.postalCode()));
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("second_city"), OXUtils::writeString(otherAddress.locality()));
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("second_state"), OXUtils::writeString(otherAddress.region()));
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("second_country"), OXUtils::writeString(otherAddress.country()));
+        }
+
+        // further information
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("note"), OXUtils::writeString(contact.note()));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("url"), OXUtils::writeString(contact.url().url().url()));
+
+        // image
+        const KContacts::Picture photo = contact.photo();
+        if (!photo.data().isNull()) {
+            QByteArray imageData;
+            QBuffer buffer(&imageData);
+            buffer.open(QIODevice::WriteOnly);
+
+            QString contentType;
+            if (!photo.data().hasAlphaChannel()) {
+                photo.data().save(&buffer, "JPEG");
+                contentType = QStringLiteral("image/jpg");
+            } else {
+                photo.data().save(&buffer, "PNG");
+                contentType = QStringLiteral("image/png");
+            }
+
+            buffer.close();
+
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("image1"), QString::fromLatin1(imageData.toBase64()));
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("image_content_type"), contentType);
+        } else {
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("image1"));
+        }
+
+        // company information
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("company"), OXUtils::writeString(contact.organization()));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("department"), OXUtils::writeString(contact.department()));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("assistants_name"), OXUtils::writeString(contact.custom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-AssistantsName"))));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("managers_name"), OXUtils::writeString(contact.custom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-ManagersName"))));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("position"), OXUtils::writeString(contact.role()));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("profession"), OXUtils::writeString(contact.custom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Profession"))));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("room_number"), OXUtils::writeString(contact.custom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Office"))));
+
+        // communication
+        const QStringList emails = contact.emails();
+        for (int i = 0; i < 3 && i < emails.count(); ++i) {
+            DAVUtils::addOxElement(document, propElement, QStringLiteral("email%1").arg(i + 1), OXUtils::writeString(emails.at(i)));
+        }
+
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("mobile1"), OXUtils::writeString(contact.phoneNumber(KContacts::PhoneNumber::Cell).number()));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("instant_messenger"), OXUtils::writeString(contact.custom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-IMAddress"))));
+
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("phone_business"), OXUtils::writeString(contact.phoneNumber(KContacts::PhoneNumber::Work).number()));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("phone_home"), OXUtils::writeString(contact.phoneNumber(KContacts::PhoneNumber::Home).number()));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("phone_other"), OXUtils::writeString(contact.phoneNumber(KContacts::PhoneNumber::Voice).number()));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("phone_car"), OXUtils::writeString(contact.phoneNumber(KContacts::PhoneNumber::Car).number()));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("fax_business"), OXUtils::writeString(contact.phoneNumber(KContacts::PhoneNumber::Fax | KContacts::PhoneNumber::Work).number()));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("fax_home"), OXUtils::writeString(contact.phoneNumber(KContacts::PhoneNumber::Fax | KContacts::PhoneNumber::Home).number()));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("fax_other"), OXUtils::writeString(contact.phoneNumber(KContacts::PhoneNumber::Fax | KContacts::PhoneNumber::Voice).number()));
+
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("pager"), OXUtils::writeString(contact.phoneNumber(KContacts::PhoneNumber::Pager).number()));
+
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("categories"), OXUtils::writeString(contact.categories().join(QStringLiteral(","))));
+    } else {
+        // it is a distribution list payload
+
+        const KContacts::ContactGroup contactGroup = object.contactGroup();
+
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("displayname"), OXUtils::writeString(contactGroup.name()));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("last_name"), OXUtils::writeString(contactGroup.name()));
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("distributionlist_flag"), OXUtils::writeBoolean(true));
+
+        QDomElement distributionList = DAVUtils::addOxElement(document, propElement, QStringLiteral("distributionlist"));
+
+        if (preloadedData) {
+            // the contact group contains contact references that has been preloaded
+            KContacts::Addressee::List *contacts = static_cast<KContacts::Addressee::List *>(preloadedData);
+            foreach (const KContacts::Addressee &contact, *contacts) {
+                QDomElement email = DAVUtils::addOxElement(document, distributionList, QStringLiteral("email"),
+                                    OXUtils::writeString(contact.preferredEmail()));
+
+                DAVUtils::setOxAttribute(email, QStringLiteral("folder_id"), OXUtils::writeNumber(0));
+                DAVUtils::setOxAttribute(email, QStringLiteral("emailfield"), OXUtils::writeNumber(0));
+                DAVUtils::setOxAttribute(email, QStringLiteral("id"), OXUtils::writeNumber(0));
+                DAVUtils::setOxAttribute(email, QStringLiteral("displayname"), OXUtils::writeString(contact.realName()));
+            }
+
+            delete contacts;
+        } else {
+            // the contact group contains only internal contact data
+            for (uint i = 0; i < contactGroup.dataCount(); ++i) {
+                const KContacts::ContactGroup::Data &data = contactGroup.data(i);
+                QDomElement email = DAVUtils::addOxElement(document, distributionList, QStringLiteral("email"),
+                                    OXUtils::writeString(data.email()));
+
+                DAVUtils::setOxAttribute(email, QStringLiteral("folder_id"), OXUtils::writeNumber(0));
+                DAVUtils::setOxAttribute(email, QStringLiteral("emailfield"), OXUtils::writeNumber(0));
+                DAVUtils::setOxAttribute(email, QStringLiteral("id"), OXUtils::writeNumber(0));
+                DAVUtils::setOxAttribute(email, QStringLiteral("displayname"), OXUtils::writeString(data.name()));
+            }
+        }
+    }
+}
+
+KJob *OXA::ContactUtils::preloadJob(const Object &object)
+{
+    Akonadi::ContactGroupExpandJob *job = new Akonadi::ContactGroupExpandJob(object.contactGroup());
+    return job;
+}
+
+void *OXA::ContactUtils::preloadData(const Object &, KJob *job)
+{
+    Akonadi::ContactGroupExpandJob *expandJob = qobject_cast<Akonadi::ContactGroupExpandJob *>(job);
+    Q_ASSERT(expandJob);
+
+    return new KContacts::Addressee::List(expandJob->contacts());
+}
diff --git a/resources/openxchange/oxa/contactutils.h b/resources/openxchange/oxa/contactutils.h
new file mode 100644 (file)
index 0000000..9df5bde
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_CONTACTUTILS_H
+#define OXA_CONTACTUTILS_H
+
+#include "object.h"
+
+class KJob;
+
+class QDomDocument;
+class QDomElement;
+
+namespace OXA
+{
+
+/**
+ * Namespace that contains helper methods for handling contacts.
+ *
+ * @author Tobias Koenig <tokoe@kde.org>
+ */
+namespace ContactUtils
+{
+/**
+ * Parses the XML tree under @p propElement and fills the contact data of @p object.
+ */
+void parseContact(const QDomElement &propElement, Object &object);
+
+/**
+ * Adds the contact data of @p object to the @p document under the @p propElement.
+ */
+void addContactElements(QDomDocument &document, QDomElement &propElement, const Object &object, void *preloadedData);
+
+KJob *preloadJob(const Object &object);
+void *preloadData(const Object &object, KJob *job);
+}
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/davmanager.cpp b/resources/openxchange/oxa/davmanager.cpp
new file mode 100644 (file)
index 0000000..e0a8a40
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "davmanager.h"
+
+#include <kio/davjob.h>
+
+#include <QtXml/QDomDocument>
+
+using namespace OXA;
+
+DavManager *DavManager::mSelf = Q_NULLPTR;
+
+DavManager::DavManager()
+{
+}
+
+DavManager::~DavManager()
+{
+}
+
+DavManager *DavManager::self()
+{
+    if (!mSelf) {
+        mSelf = new DavManager();
+    }
+
+    return mSelf;
+}
+
+void DavManager::setBaseUrl(const QUrl &url)
+{
+    mBaseUrl = url;
+}
+
+QUrl DavManager::baseUrl() const
+{
+    return mBaseUrl;
+}
+
+KIO::DavJob *DavManager::createFindJob(const QString &path, const QDomDocument &document) const
+{
+    QUrl url(mBaseUrl);
+    url.setPath(path);
+
+    return KIO::davPropFind(url, document, QStringLiteral("0"), KIO::HideProgressInfo);
+}
+
+KIO::DavJob *DavManager::createPatchJob(const QString &path, const QDomDocument &document) const
+{
+    QUrl url(mBaseUrl);
+    url.setPath(path);
+
+    return KIO::davPropPatch(url, document, KIO::HideProgressInfo);
+}
diff --git a/resources/openxchange/oxa/davmanager.h b/resources/openxchange/oxa/davmanager.h
new file mode 100644 (file)
index 0000000..bca6f82
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_DAVMANAGER_H
+#define OXA_DAVMANAGER_H
+
+#include <qurl.h>
+
+namespace KIO
+{
+class DavJob;
+}
+
+class QDomDocument;
+
+namespace OXA
+{
+
+/**
+ * @short A class that manages DAV specific information.
+ *
+ * The DavManager stores the base url of the DAV service that
+ * shall be accessed and provides factory methods for creating
+ * DAV find and patch jobs.
+ *
+ * @author Tobias Koenig <tokoe@kde.org>
+ */
+class DavManager
+{
+public:
+    /**
+     * Destroys the DAV manager.
+     */
+    ~DavManager();
+
+    /**
+     * Returns the global instance of the DAV manager.
+     */
+    static DavManager *self();
+
+    /**
+     * Sets the base @p url the DAV manager should use.
+     */
+    void setBaseUrl(const QUrl &url);
+
+    /**
+     * Returns the base url the DAV manager uses.
+     */
+    QUrl baseUrl() const;
+
+    /**
+     * Returns a new DAV find job.
+     *
+     * @param path The path that is appended to the base url.
+     * @param document The request XML document.
+     */
+    KIO::DavJob *createFindJob(const QString &path, const QDomDocument &document) const;
+
+    /**
+     * Returns a new DAV patch job.
+     *
+     * @param path The path that is appended to the base url.
+     * @param document The request XML document.
+     */
+    KIO::DavJob *createPatchJob(const QString &path, const QDomDocument &document) const;
+
+private:
+    /**
+     * Creates a new DAV manager.
+     */
+    DavManager();
+
+    QUrl mBaseUrl;
+    static DavManager *mSelf;
+};
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/davutils.cpp b/resources/openxchange/oxa/davutils.cpp
new file mode 100644 (file)
index 0000000..b63c2e3
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "davutils.h"
+
+using namespace OXA;
+
+QDomElement DAVUtils::addDavElement(QDomDocument &document, QDomNode &parentNode, const QString &tag)
+{
+    const QDomElement element = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("D:") + tag);
+    parentNode.appendChild(element);
+
+    return element;
+}
+
+QDomElement DAVUtils::addOxElement(QDomDocument &document, QDomNode &parentNode, const QString &tag, const QString &text)
+{
+    QDomElement element = document.createElementNS(QStringLiteral("http://www.open-xchange.org"), QStringLiteral("ox:") + tag);
+
+    if (!text.isEmpty()) {
+        const QDomText textNode = document.createTextNode(text);
+        element.appendChild(textNode);
+    }
+
+    parentNode.appendChild(element);
+
+    return element;
+}
+
+void DAVUtils::setOxAttribute(QDomElement &element, const QString &name, const QString &value)
+{
+    element.setAttributeNS(QStringLiteral("http://www.open-xchange.org"), QStringLiteral("ox:") + name, value);
+}
+
+bool DAVUtils::davErrorOccurred(const QDomDocument &document, QString &errorText, QString &errorStatus)
+{
+    const QDomElement documentElement = document.documentElement();
+    const QDomNodeList propStats = documentElement.elementsByTagNameNS(QStringLiteral("DAV:"),
+                                   QStringLiteral("propstat"));
+
+    for (int i = 0; i < propStats.count(); ++i) {
+        const QDomElement propStat = propStats.at(i).toElement();
+        const QDomElement status = propStat.firstChildElement(QStringLiteral("status"));
+        const QDomElement description = propStat.firstChildElement(QStringLiteral("responsedescription"));
+
+        if (status.text() != QLatin1String("200")) {
+            errorText = description.text();
+            errorStatus = status.text();
+            return true;
+        }
+    }
+
+    return false;
+}
diff --git a/resources/openxchange/oxa/davutils.h b/resources/openxchange/oxa/davutils.h
new file mode 100644 (file)
index 0000000..2838794
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_DAVUTILS_H
+#define OXA_DAVUTILS_H
+
+#include <QtCore/QString>
+#include <QtXml/QDomDocument>
+#include <QtXml/QDomElement>
+#include <QtXml/QDomNode>
+
+namespace OXA
+{
+
+/**
+ * Namespace that contains methods for creating or modifying DAV XML documents.
+ *
+ * @author Tobias Koenig <tokoe@kde.org>
+ */
+namespace DAVUtils
+{
+/**
+ * Adds a new element with the given @p tag inside the DAV namespace under @p parentNode
+ * to the @p document.
+ *
+ * @return The newly added element.
+ */
+QDomElement addDavElement(QDomDocument &document, QDomNode &parentNode, const QString &tag);
+
+/**
+ * Adds a new element with the given @p tag and @p value inside the OX namespace under @p parentNode
+ * to the @p document.
+ *
+ * @return The newly added element.
+ */
+QDomElement addOxElement(QDomDocument &document, QDomNode &parentNode, const QString &tag, const QString &text = QString());
+
+/**
+ * Sets the attribute of @p element inside the OX namespace with the given @p name to @p value.
+ */
+void setOxAttribute(QDomElement &element, const QString &name, const QString &value);
+
+/**
+ * Checks whether the response @p document contains an error message.
+ * If so, @c true is returned, @p errorText set to the error message and @p errorStatus set to error status.
+ */
+bool davErrorOccurred(const QDomDocument &document, QString &errorText, QString &errorStatus);
+}
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/folder.cpp b/resources/openxchange/oxa/folder.cpp
new file mode 100644 (file)
index 0000000..f41e014
--- /dev/null
@@ -0,0 +1,198 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "folder.h"
+
+using namespace OXA;
+
+Folder::Permissions::Permissions()
+    : mFolderPermission(NoPermission),
+      mObjectReadPermission(NoReadPermission),
+      mObjectWritePermission(NoWritePermission),
+      mObjectDeletePermission(NoDeletePermission),
+      mAdminFlag(false)
+{
+}
+
+void Folder::Permissions::setFolderPermission(FolderPermission permission)
+{
+    mFolderPermission = permission;
+}
+
+Folder::Permissions::FolderPermission Folder::Permissions::folderPermission() const
+{
+    return mFolderPermission;
+}
+
+void Folder::Permissions::setObjectReadPermission(ObjectReadPermission permission)
+{
+    mObjectReadPermission = permission;
+}
+
+Folder::Permissions::ObjectReadPermission Folder::Permissions::objectReadPermission() const
+{
+    return mObjectReadPermission;
+}
+
+void Folder::Permissions::setObjectWritePermission(ObjectWritePermission permission)
+{
+    mObjectWritePermission = permission;
+}
+
+Folder::Permissions::ObjectWritePermission Folder::Permissions::objectWritePermission() const
+{
+    return mObjectWritePermission;
+}
+
+void Folder::Permissions::setObjectDeletePermission(ObjectDeletePermission permission)
+{
+    mObjectDeletePermission = permission;
+}
+
+Folder::Permissions::ObjectDeletePermission Folder::Permissions::objectDeletePermission() const
+{
+    return mObjectDeletePermission;
+}
+
+void Folder::Permissions::setAdminFlag(bool value)
+{
+    mAdminFlag = value;
+}
+
+bool Folder::Permissions::adminFlag() const
+{
+    return mAdminFlag;
+}
+
+Folder::Folder()
+    : mObjectId(-1), mFolderId(-1)
+{
+}
+
+void Folder::setObjectStatus(ObjectStatus status)
+{
+    mObjectStatus = status;
+}
+
+Folder::ObjectStatus Folder::objectStatus() const
+{
+    return mObjectStatus;
+}
+
+void Folder::setTitle(const QString &title)
+{
+    mTitle = title;
+}
+
+QString Folder::title() const
+{
+    return mTitle;
+}
+
+void Folder::setType(Type type)
+{
+    mType = type;
+}
+
+Folder::Type Folder::type() const
+{
+    return mType;
+}
+
+void Folder::setModule(Module module)
+{
+    mModule = module;
+}
+
+Folder::Module Folder::module() const
+{
+    return mModule;
+}
+
+void Folder::setObjectId(qlonglong id)
+{
+    mObjectId = id;
+}
+
+qlonglong Folder::objectId() const
+{
+    return mObjectId;
+}
+
+void Folder::setFolderId(qlonglong id)
+{
+    mFolderId = id;
+}
+
+qlonglong Folder::folderId() const
+{
+    return mFolderId;
+}
+
+void Folder::setIsDefaultFolder(bool value)
+{
+    mIsDefaultFolder = value;
+}
+
+bool Folder::isDefaultFolder() const
+{
+    return mIsDefaultFolder;
+}
+
+void Folder::setOwner(qlonglong id)
+{
+    mOwner = id;
+}
+
+qlonglong Folder::owner() const
+{
+    return mOwner;
+}
+
+void Folder::setLastModified(const QString &timeStamp)
+{
+    mLastModified = timeStamp;
+}
+
+QString Folder::lastModified() const
+{
+    return mLastModified;
+}
+
+void Folder::setUserPermissions(const UserPermissions &permissions)
+{
+    mUserPermissions = permissions;
+}
+
+Folder::UserPermissions Folder::userPermissions() const
+{
+    return mUserPermissions;
+}
+
+void Folder::setGroupPermissions(const GroupPermissions &permissions)
+{
+    mGroupPermissions = permissions;
+}
+
+Folder::GroupPermissions Folder::groupPermissions() const
+{
+    return mGroupPermissions;
+}
diff --git a/resources/openxchange/oxa/folder.h b/resources/openxchange/oxa/folder.h
new file mode 100644 (file)
index 0000000..32684da
--- /dev/null
@@ -0,0 +1,199 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_FOLDER_H
+#define OXA_FOLDER_H
+
+#include <QtCore/QVector>
+#include <QtCore/QMap>
+#include <QtCore/QString>
+
+namespace OXA
+{
+
+/**
+ * @short A class that contains information about folders on the OX server.
+ *
+ * @author Tobias Koenig <tokoe@kde.org>
+ */
+class Folder
+{
+public:
+    /**
+     * Describes a list of folders.
+     */
+    typedef QVector<Folder> List;
+
+    /**
+     * Describes the status of the folder.
+     */
+    enum ObjectStatus {
+        Created, ///< The folder has been created or modified.
+        Deleted  ///< The folder has been deleted.
+    };
+
+    /**
+     * Describes the visibility type of the folder.
+     */
+    enum Type {
+        Public,  ///< The folder is visible for all users.
+        Private  ///< The folder is only visible for the owner.
+    };
+
+    /**
+     * Describes the module the folder belongs to.
+     */
+    enum Module {
+        Unbound,  ///< The folder is only a structural folder.
+        Calendar, ///< The folder contains events.
+        Contacts, ///< The folder contains contacts.
+        Tasks     ///< The folder contains tasks.
+    };
+
+    /**
+     * Describes the permissions a user or group can have on
+     * a folder.
+     */
+    class Permissions
+    {
+    public:
+        /**
+         * Describes the permissions on folder objects.
+         */
+        enum FolderPermission {
+            NoPermission = 0,     ///< No permissions.
+            FolderIsVisible = 2,  ///< The folder can be read.
+            CreateObjects = 4,    ///< Objects can be created in the folder.
+            CreateSubfolders = 8, ///< Subfolders can be created in the folder.
+            AdminPermission = 128 ///< Permissions can be changed.
+        };
+
+        /**
+         * Describes the read permissions on other objects.
+         */
+        enum ObjectReadPermission {
+            NoReadPermission = 0,     ///< The objects can not be read.
+            ReadOwnObjects = 2,       ///< Only own objects can be read.
+            ReadAllObjects = 4,       ///< All objects can be read.
+            AdminReadPermission = 128
+        };
+
+        /**
+         * Describes the write permissions on other objects.
+         */
+        enum ObjectWritePermission {
+            NoWritePermission = 0,     ///< The objects can not be written.
+            WriteOwnObjects = 2,       ///< Only own objects can be written.
+            WriteAllObjects = 4,       ///< All objects can be written.
+            AdminWritePermission = 128
+        };
+
+        /**
+         * Describes the delete permissions on other objects.
+         */
+        enum ObjectDeletePermission {
+            NoDeletePermission = 0,     ///< The objects can not be deleted.
+            DeleteOwnObjects = 2,       ///< Only own objects can be deleted.
+            DeleteAllObjects = 4,       ///< All objects can be deleted.
+            AdminDeletePermission = 128
+        };
+
+        Permissions();
+
+        void setFolderPermission(FolderPermission permission);
+        FolderPermission folderPermission() const;
+
+        void setObjectReadPermission(ObjectReadPermission permission);
+        ObjectReadPermission objectReadPermission() const;
+
+        void setObjectWritePermission(ObjectWritePermission permission);
+        ObjectWritePermission objectWritePermission() const;
+
+        void setObjectDeletePermission(ObjectDeletePermission permission);
+        ObjectDeletePermission objectDeletePermission() const;
+
+        void setAdminFlag(bool value);
+        bool adminFlag() const;
+
+    private:
+        FolderPermission mFolderPermission;
+        ObjectReadPermission mObjectReadPermission;
+        ObjectWritePermission mObjectWritePermission;
+        ObjectDeletePermission mObjectDeletePermission;
+        bool mAdminFlag;
+    };
+
+    typedef QMap<qlonglong, Permissions> UserPermissions;
+    typedef QMap<qlonglong, Permissions> GroupPermissions;
+
+    Folder();
+
+    void setObjectStatus(ObjectStatus status);
+    ObjectStatus objectStatus() const;
+
+    void setTitle(const QString &title);
+    QString title() const;
+
+    void setType(Type type);
+    Type type() const;
+
+    void setModule(Module module);
+    Module module() const;
+
+    void setObjectId(qlonglong id);
+    qlonglong objectId() const;
+
+    void setFolderId(qlonglong id);
+    qlonglong folderId() const;
+
+    void setIsDefaultFolder(bool value);
+    bool isDefaultFolder() const;
+
+    void setOwner(qlonglong id);
+    qlonglong owner() const;
+
+    void setLastModified(const QString &timeStamp);
+    QString lastModified() const;
+
+    void setUserPermissions(const UserPermissions &permissions);
+    UserPermissions userPermissions() const;
+
+    void setGroupPermissions(const GroupPermissions &permissions);
+    GroupPermissions groupPermissions() const;
+
+private:
+    ObjectStatus mObjectStatus;
+    QString mTitle;
+    Type mType;
+    Module mModule;
+    qlonglong mObjectId;
+    qlonglong mFolderId;
+    bool mIsDefaultFolder;
+    qlonglong mOwner;
+    QString mLastModified;
+    UserPermissions mUserPermissions;
+    GroupPermissions mGroupPermissions;
+};
+
+}
+Q_DECLARE_TYPEINFO(OXA::Folder, Q_MOVABLE_TYPE);
+
+#endif
diff --git a/resources/openxchange/oxa/foldercreatejob.cpp b/resources/openxchange/oxa/foldercreatejob.cpp
new file mode 100644 (file)
index 0000000..ab8ea7c
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "foldercreatejob.h"
+
+#include "davmanager.h"
+#include "davutils.h"
+#include "folderutils.h"
+#include "oxutils.h"
+
+#include <kio/davjob.h>
+
+using namespace OXA;
+
+FolderCreateJob::FolderCreateJob(const Folder &folder, QObject *parent)
+    : KJob(parent), mFolder(folder)
+{
+}
+
+void FolderCreateJob::start()
+{
+    QDomDocument document;
+    QDomElement propertyupdate = DAVUtils::addDavElement(document, document, QStringLiteral("propertyupdate"));
+    QDomElement set = DAVUtils::addDavElement(document, propertyupdate, QStringLiteral("set"));
+    QDomElement prop = DAVUtils::addDavElement(document, set, QStringLiteral("prop"));
+
+    FolderUtils::addFolderElements(document, prop, mFolder);
+
+    const QString path = QStringLiteral("/servlet/webdav.folders");
+
+    KIO::DavJob *job = DavManager::self()->createPatchJob(path, document);
+    connect(job, &KIO::DavJob::result, this, &FolderCreateJob::davJobFinished);
+}
+
+Folder FolderCreateJob::folder() const
+{
+    return mFolder;
+}
+
+void FolderCreateJob::davJobFinished(KJob *job)
+{
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+        emitResult();
+        return;
+    }
+
+    KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
+
+    const QDomDocument document = davJob->response();
+
+    QString errorText, errorStatus;
+    if (DAVUtils::davErrorOccurred(document, errorText, errorStatus)) {
+        setError(UserDefinedError);
+        setErrorText(errorText);
+        emitResult();
+        return;
+    }
+
+    QDomElement multistatus = document.documentElement();
+    QDomElement response = multistatus.firstChildElement(QStringLiteral("response"));
+    const QDomNodeList props = response.elementsByTagName(QStringLiteral("prop"));
+    const QDomElement prop = props.at(0).toElement();
+
+    QDomElement element = prop.firstChildElement();
+    while (!element.isNull()) {
+        if (element.tagName() == QLatin1String("object_id")) {
+            mFolder.setObjectId(OXUtils::readNumber(element.text()));
+        } else if (element.tagName() == QLatin1String("last_modified")) {
+            mFolder.setLastModified(OXUtils::readString(element.text()));
+        }
+
+        element = element.nextSiblingElement();
+    }
+
+    emitResult();
+}
+
diff --git a/resources/openxchange/oxa/foldercreatejob.h b/resources/openxchange/oxa/foldercreatejob.h
new file mode 100644 (file)
index 0000000..3783214
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_FOLDERCREATEJOB_H
+#define OXA_FOLDERCREATEJOB_H
+
+#include <kjob.h>
+
+#include "folder.h"
+
+namespace OXA
+{
+
+/**
+ * @short A job that creates a new folder on the OX server.
+ *
+ * @author Tobias Koenig <tokoe@kde.org>
+ */
+class FolderCreateJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new folder create job.
+     *
+     * @param folder The folder to create.
+     * @param parent The parent object.
+     */
+    explicit FolderCreateJob(const Folder &folder, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Starts the job.
+     */
+    void start() Q_DECL_OVERRIDE;
+
+    /**
+     * Returns the updated folder that has been created.
+     */
+    Folder folder() const;
+
+private Q_SLOTS:
+    void davJobFinished(KJob *);
+
+private:
+    Folder mFolder;
+};
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/folderdeletejob.cpp b/resources/openxchange/oxa/folderdeletejob.cpp
new file mode 100644 (file)
index 0000000..5b546b3
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "folderdeletejob.h"
+
+#include "davmanager.h"
+#include "davutils.h"
+#include "oxutils.h"
+
+#include <kio/davjob.h>
+
+#include <QtCore/QDebug>
+
+using namespace OXA;
+
+FolderDeleteJob::FolderDeleteJob(const Folder &folder, QObject *parent)
+    : KJob(parent), mFolder(folder)
+{
+}
+
+void FolderDeleteJob::start()
+{
+    QDomDocument document;
+    QDomElement propertyupdate = DAVUtils::addDavElement(document, document, QStringLiteral("propertyupdate"));
+    QDomElement set = DAVUtils::addDavElement(document, propertyupdate, QStringLiteral("set"));
+    QDomElement prop = DAVUtils::addDavElement(document, set, QStringLiteral("prop"));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("object_id"), OXUtils::writeNumber(mFolder.objectId()));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("method"), OXUtils::writeString(QStringLiteral("DELETE")));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("last_modified"), OXUtils::writeString(mFolder.lastModified()));
+
+    const QString path = QStringLiteral("/servlet/webdav.folders");
+
+    KIO::DavJob *job = DavManager::self()->createPatchJob(path, document);
+    connect(job, &KIO::DavJob::result, this, &FolderDeleteJob::davJobFinished);
+}
+
+void FolderDeleteJob::davJobFinished(KJob *job)
+{
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+        emitResult();
+        return;
+    }
+
+    KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
+
+    const QDomDocument document = davJob->response();
+
+    QString errorText, errorStatus;
+    if (DAVUtils::davErrorOccurred(document, errorText, errorStatus)) {
+        setError(UserDefinedError);
+        setErrorText(errorText);
+        emitResult();
+        return;
+    }
+
+    emitResult();
+}
+
diff --git a/resources/openxchange/oxa/folderdeletejob.h b/resources/openxchange/oxa/folderdeletejob.h
new file mode 100644 (file)
index 0000000..9895184
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_FOLDERDELETEJOB_H
+#define OXA_FOLDERDELETEJOB_H
+
+#include "folder.h"
+
+#include <kjob.h>
+
+namespace OXA
+{
+
+/**
+ * @short A job that deletes a folder on the OX server.
+ *
+ * @author Tobias Koenig <tokoe@kde.org>
+ */
+class FolderDeleteJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new folder delete job.
+     *
+     * @param folder The folder to delete.
+     * @param parent The parent object.
+     *
+     * @note The folder needs the objectId, folderId and lastModified property set.
+     */
+    explicit FolderDeleteJob(const Folder &folder, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Starts the job.
+     */
+    void start() Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void davJobFinished(KJob *);
+
+private:
+    Folder mFolder;
+};
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/foldermodifyjob.cpp b/resources/openxchange/oxa/foldermodifyjob.cpp
new file mode 100644 (file)
index 0000000..3d3734f
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "foldermodifyjob.h"
+
+#include "davmanager.h"
+#include "davutils.h"
+#include "oxutils.h"
+
+#include <kio/davjob.h>
+
+using namespace OXA;
+
+FolderModifyJob::FolderModifyJob(const Folder &folder, QObject *parent)
+    : KJob(parent), mFolder(folder)
+{
+}
+
+void FolderModifyJob::start()
+{
+    QDomDocument document;
+    QDomElement propertyupdate = DAVUtils::addDavElement(document, document, QStringLiteral("propertyupdate"));
+    QDomElement set = DAVUtils::addDavElement(document, propertyupdate, QStringLiteral("set"));
+    QDomElement prop = DAVUtils::addDavElement(document, set, QStringLiteral("prop"));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("title"), OXUtils::writeString(mFolder.title()));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("object_id"), OXUtils::writeNumber(mFolder.objectId()));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("folder_id"), OXUtils::writeNumber(mFolder.folderId()));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("last_modified"), OXUtils::writeString(mFolder.lastModified()));
+
+    const QString path = QStringLiteral("/servlet/webdav.folders");
+
+    KIO::DavJob *job = DavManager::self()->createPatchJob(path, document);
+    connect(job, &KIO::DavJob::result, this, &FolderModifyJob::davJobFinished);
+}
+
+Folder FolderModifyJob::folder() const
+{
+    return mFolder;
+}
+
+void FolderModifyJob::davJobFinished(KJob *job)
+{
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+        emitResult();
+        return;
+    }
+
+    KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
+
+    const QDomDocument document = davJob->response();
+
+    QString errorText, errorStatus;
+    if (DAVUtils::davErrorOccurred(document, errorText, errorStatus)) {
+        setError(UserDefinedError);
+        setErrorText(errorText);
+        emitResult();
+        return;
+    }
+
+    QDomElement multistatus = document.documentElement();
+    QDomElement response = multistatus.firstChildElement(QStringLiteral("response"));
+    const QDomNodeList props = response.elementsByTagName(QStringLiteral("prop"));
+    const QDomElement prop = props.at(0).toElement();
+
+    QDomElement element = prop.firstChildElement();
+    while (!element.isNull()) {
+        if (element.tagName() == QLatin1String("last_modified")) {
+            mFolder.setLastModified(OXUtils::readString(element.text()));
+        }
+
+        element = element.nextSiblingElement();
+    }
+
+    emitResult();
+}
+
diff --git a/resources/openxchange/oxa/foldermodifyjob.h b/resources/openxchange/oxa/foldermodifyjob.h
new file mode 100644 (file)
index 0000000..3db14ad
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_FOLDERMODIFYJOB_H
+#define OXA_FOLDERMODIFYJOB_H
+
+#include <kjob.h>
+
+#include "folder.h"
+
+namespace OXA
+{
+
+/**
+ * @short A job that modifies a folder on the OX server.
+ *
+ * @author Tobias Koenig <tokoe@kde.org>
+ */
+class FolderModifyJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new folder modify job.
+     *
+     * @param folder The folder to modify.
+     * @param parent The parent object.
+     *
+     * @note The folder needs at least the objectId and lastModified property set.
+     */
+    explicit FolderModifyJob(const Folder &folder, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Starts the job.
+     */
+    void start() Q_DECL_OVERRIDE;
+
+    /**
+     * Returns the updated folder that has been modified.
+     */
+    Folder folder() const;
+
+private Q_SLOTS:
+    void davJobFinished(KJob *);
+
+private:
+    Folder mFolder;
+};
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/foldermovejob.cpp b/resources/openxchange/oxa/foldermovejob.cpp
new file mode 100644 (file)
index 0000000..f545609
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "foldermovejob.h"
+
+#include "davmanager.h"
+#include "davutils.h"
+#include "oxutils.h"
+
+#include <kio/davjob.h>
+
+using namespace OXA;
+
+FolderMoveJob::FolderMoveJob(const Folder &folder, const Folder &destinationFolder, QObject *parent)
+    : KJob(parent), mFolder(folder), mDestinationFolder(destinationFolder)
+{
+}
+
+void FolderMoveJob::start()
+{
+    QDomDocument document;
+    QDomElement propertyupdate = DAVUtils::addDavElement(document, document, QStringLiteral("propertyupdate"));
+    QDomElement set = DAVUtils::addDavElement(document, propertyupdate, QStringLiteral("set"));
+    QDomElement prop = DAVUtils::addDavElement(document, set, QStringLiteral("prop"));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("object_id"), OXUtils::writeNumber(mFolder.objectId()));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("folder_id"), OXUtils::writeNumber(mFolder.folderId()));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("last_modified"), OXUtils::writeString(mFolder.lastModified()));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("folder"), OXUtils::writeNumber(mDestinationFolder.objectId()));
+
+    const QString path = QStringLiteral("/servlet/webdav.folders");
+
+    KIO::DavJob *job = DavManager::self()->createPatchJob(path, document);
+    connect(job, &KIO::DavJob::result, this, &FolderMoveJob::davJobFinished);
+}
+
+Folder FolderMoveJob::folder() const
+{
+    return mFolder;
+}
+
+void FolderMoveJob::davJobFinished(KJob *job)
+{
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+        emitResult();
+        return;
+    }
+
+    KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
+
+    const QDomDocument document = davJob->response();
+
+    QString errorText, errorStatus;
+    if (DAVUtils::davErrorOccurred(document, errorText, errorStatus)) {
+        setError(UserDefinedError);
+        setErrorText(errorText);
+        emitResult();
+        return;
+    }
+
+    QDomElement multistatus = document.documentElement();
+    QDomElement response = multistatus.firstChildElement(QStringLiteral("response"));
+    const QDomNodeList props = response.elementsByTagName(QStringLiteral("prop"));
+    const QDomElement prop = props.at(0).toElement();
+
+    QDomElement element = prop.firstChildElement();
+    while (!element.isNull()) {
+        if (element.tagName() == QLatin1String("last_modified")) {
+            mFolder.setLastModified(OXUtils::readString(element.text()));
+        }
+
+        element = element.nextSiblingElement();
+    }
+
+    emitResult();
+}
+
diff --git a/resources/openxchange/oxa/foldermovejob.h b/resources/openxchange/oxa/foldermovejob.h
new file mode 100644 (file)
index 0000000..f610b57
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_FOLDERMOVEJOB_H
+#define OXA_FOLDERMOVEJOB_H
+
+#include <kjob.h>
+
+#include "folder.h"
+
+namespace OXA
+{
+
+/**
+ * @short A job that moves a folder on the OX server.
+ *
+ * @author Tobias Koenig <tokoe@kde.org>
+ */
+class FolderMoveJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new folder move job.
+     *
+     * @param folder The folder to move.
+     * @param destinationFolder The new parent folder.
+     * @param parent The parent object.
+     *
+     * @note The folder needs the objectId, folderId and lastModified property set, the
+     *       destinationFolder the objectId property.
+     */
+    FolderMoveJob(const Folder &folder, const Folder &destinationFolder, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Starts the job.
+     */
+    void start() Q_DECL_OVERRIDE;
+
+    /**
+     * Returns the updated folder that has been moved.
+     */
+    Folder folder() const;
+
+private Q_SLOTS:
+    void davJobFinished(KJob *);
+
+private:
+    Folder mFolder;
+    Folder mDestinationFolder;
+};
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/folderrequestjob.cpp b/resources/openxchange/oxa/folderrequestjob.cpp
new file mode 100644 (file)
index 0000000..f6fd541
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "folderrequestjob.h"
+
+#include "davmanager.h"
+#include "davutils.h"
+#include "folderutils.h"
+#include "oxutils.h"
+
+#include <kio/davjob.h>
+
+using namespace OXA;
+
+FolderRequestJob::FolderRequestJob(const Folder &folder, QObject *parent)
+    : KJob(parent), mFolder(folder)
+{
+}
+
+void FolderRequestJob::start()
+{
+    QDomDocument document;
+    QDomElement multistatus = DAVUtils::addDavElement(document, document, QStringLiteral("multistatus"));
+    QDomElement prop = DAVUtils::addDavElement(document, multistatus, QStringLiteral("prop"));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("object_id"), OXUtils::writeNumber(mFolder.objectId()));
+
+    const QString path = QStringLiteral("/servlet/webdav.folders");
+
+    KIO::DavJob *job = DavManager::self()->createFindJob(path, document);
+    connect(job, &KIO::DavJob::result, this, &FolderRequestJob::davJobFinished);
+}
+
+Folder FolderRequestJob::folder() const
+{
+    return mFolder;
+}
+
+void FolderRequestJob::davJobFinished(KJob *job)
+{
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+        emitResult();
+        return;
+    }
+
+    KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
+
+    const QDomDocument document = davJob->response();
+
+    QString errorText, errorStatus;
+    if (DAVUtils::davErrorOccurred(document, errorText, errorStatus)) {
+        setError(UserDefinedError);
+        setErrorText(errorText);
+        emitResult();
+        return;
+    }
+
+    QDomElement multistatus = document.documentElement();
+    QDomElement response = multistatus.firstChildElement(QStringLiteral("response"));
+    const QDomNodeList props = response.elementsByTagName(QStringLiteral("prop"));
+    const QDomElement prop = props.at(0).toElement();
+    mFolder = FolderUtils::parseFolder(prop);
+
+    emitResult();
+}
+
diff --git a/resources/openxchange/oxa/folderrequestjob.h b/resources/openxchange/oxa/folderrequestjob.h
new file mode 100644 (file)
index 0000000..d4227d5
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_FOLDERREQUESTJOB_H
+#define OXA_FOLDERREQUESTJOB_H
+
+#include <kjob.h>
+
+#include "folder.h"
+
+namespace OXA
+{
+
+/**
+ * @short A job that requests a folder from the OX server.
+ *
+ * @author Tobias Koenig <tokoe@kde.org>
+ */
+class FolderRequestJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new folder request job.
+     *
+     * @param folder The folder to request.
+     * @param parent The parent object.
+     *
+     * @note The folder needs the objectId property set.
+     */
+    explicit FolderRequestJob(const Folder &folder, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Starts the job.
+     */
+    void start() Q_DECL_OVERRIDE;
+
+    /**
+     * Returns the requested folder.
+     */
+    Folder folder() const;
+
+private Q_SLOTS:
+    void davJobFinished(KJob *);
+
+private:
+    Folder mFolder;
+};
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/foldersrequestdeltajob.cpp b/resources/openxchange/oxa/foldersrequestdeltajob.cpp
new file mode 100644 (file)
index 0000000..e40303c
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "foldersrequestdeltajob.h"
+
+#include "foldersrequestjob.h"
+
+#include <QtCore/QDebug>
+
+using namespace OXA;
+
+FoldersRequestDeltaJob::FoldersRequestDeltaJob(qulonglong lastSync, QObject *parent)
+    : KJob(parent), mLastSync(lastSync), mJobFinishedCount(0)
+{
+}
+
+void FoldersRequestDeltaJob::start()
+{
+    FoldersRequestJob *modifiedJob = new FoldersRequestJob(mLastSync, FoldersRequestJob::Modified, this);
+    connect(modifiedJob, &FoldersRequestJob::result, this, &FoldersRequestDeltaJob::fetchModifiedJobFinished);
+    modifiedJob->start();
+
+    FoldersRequestJob *deletedJob = new FoldersRequestJob(mLastSync, FoldersRequestJob::Deleted, this);
+    connect(deletedJob, &FoldersRequestJob::result, this, &FoldersRequestDeltaJob::fetchDeletedJobFinished);
+    deletedJob->start();
+}
+
+Folder::List FoldersRequestDeltaJob::modifiedFolders() const
+{
+    return mModifiedFolders;
+}
+
+Folder::List FoldersRequestDeltaJob::deletedFolders() const
+{
+    return mDeletedFolders;
+}
+
+void FoldersRequestDeltaJob::fetchModifiedJobFinished(KJob *job)
+{
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+        emitResult();
+        return;
+    }
+
+    const FoldersRequestJob *requestJob = qobject_cast<FoldersRequestJob *>(job);
+
+    mModifiedFolders << requestJob->folders();
+
+    mJobFinishedCount++;
+
+    if (mJobFinishedCount == 2) {
+        emitResult();
+    }
+}
+
+void FoldersRequestDeltaJob::fetchDeletedJobFinished(KJob *job)
+{
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+        emitResult();
+        return;
+    }
+
+    const FoldersRequestJob *requestJob = qobject_cast<FoldersRequestJob *>(job);
+
+    mDeletedFolders << requestJob->folders();
+
+    mJobFinishedCount++;
+
+    if (mJobFinishedCount == 2) {
+        emitResult();
+    }
+}
+
diff --git a/resources/openxchange/oxa/foldersrequestdeltajob.h b/resources/openxchange/oxa/foldersrequestdeltajob.h
new file mode 100644 (file)
index 0000000..830ef61
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_FOLDERSREQUESTDELTAJOB_H
+#define OXA_FOLDERSREQUESTDELTAJOB_H
+
+#include <kjob.h>
+
+#include "folder.h"
+
+namespace OXA
+{
+
+/**
+ * @short A job that requests the delta for folders changes from the OX server.
+ *
+ * @author Tobias Koenig <tokoe@kde.org>
+ */
+class FoldersRequestDeltaJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new folders request delta job.
+     *
+     * @param lastSync The timestamp of the last sync. Only added, modified and deleted folders
+     *                 after this date will be requested. 0 will request all available folders.
+     * @param parent The parent object.
+     */
+    explicit FoldersRequestDeltaJob(qulonglong lastSync, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Starts the job.
+     */
+    void start() Q_DECL_OVERRIDE;
+
+    /**
+     * Returns the list of all added and modified folders.
+     */
+    Folder::List modifiedFolders() const;
+
+    /**
+     * Returns the list of all deleted folders.
+     */
+    Folder::List deletedFolders() const;
+
+private Q_SLOTS:
+    void fetchModifiedJobFinished(KJob *);
+    void fetchDeletedJobFinished(KJob *);
+
+private:
+    qulonglong mLastSync;
+    Folder::List mModifiedFolders;
+    Folder::List mDeletedFolders;
+    int mJobFinishedCount;
+};
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/foldersrequestjob.cpp b/resources/openxchange/oxa/foldersrequestjob.cpp
new file mode 100644 (file)
index 0000000..87021a6
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "foldersrequestjob.h"
+
+#include "davmanager.h"
+#include "davutils.h"
+#include "folderutils.h"
+#include "oxutils.h"
+
+#include <kio/davjob.h>
+
+#include <QtCore/QDebug>
+
+using namespace OXA;
+
+FoldersRequestJob::FoldersRequestJob(qulonglong lastSync, Mode mode, QObject *parent)
+    : KJob(parent), mLastSync(lastSync), mMode(mode)
+{
+}
+
+void FoldersRequestJob::start()
+{
+    QDomDocument document;
+    QDomElement multistatus = DAVUtils::addDavElement(document, document, QStringLiteral("multistatus"));
+    QDomElement prop = DAVUtils::addDavElement(document, multistatus, QStringLiteral("prop"));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("lastsync"), OXUtils::writeNumber(mLastSync));
+    if (mMode == Modified) {
+        DAVUtils::addOxElement(document, prop, QStringLiteral("objectmode"), QStringLiteral("MODIFIED"));
+    } else {
+        DAVUtils::addOxElement(document, prop, QStringLiteral("objectmode"), QStringLiteral("DELETED"));
+    }
+
+    const QString path = QStringLiteral("/servlet/webdav.folders");
+
+    KIO::DavJob *job = DavManager::self()->createFindJob(path, document);
+    connect(job, &KIO::DavJob::result, this, &FoldersRequestJob::davJobFinished);
+}
+
+Folder::List FoldersRequestJob::folders() const
+{
+    return mFolders;
+}
+
+void FoldersRequestJob::davJobFinished(KJob *job)
+{
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+        emitResult();
+        return;
+    }
+
+    KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
+
+    const QDomDocument document = davJob->response();
+
+    QString errorText, errorStatus;
+    if (DAVUtils::davErrorOccurred(document, errorText, errorStatus)) {
+        setError(UserDefinedError);
+        setErrorText(errorText);
+        emitResult();
+        return;
+    }
+
+    QDomElement multistatus = document.documentElement();
+    QDomElement response = multistatus.firstChildElement(QStringLiteral("response"));
+    while (!response.isNull()) {
+        const QDomNodeList props = response.elementsByTagName(QStringLiteral("prop"));
+        const QDomElement prop = props.at(0).toElement();
+        mFolders.append(FolderUtils::parseFolder(prop));
+        response = response.nextSiblingElement();
+    }
+
+    emitResult();
+}
+
diff --git a/resources/openxchange/oxa/foldersrequestjob.h b/resources/openxchange/oxa/foldersrequestjob.h
new file mode 100644 (file)
index 0000000..4b1ca78
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_FOLDERSREQUESTJOB_H
+#define OXA_FOLDERSREQUESTJOB_H
+
+#include <kjob.h>
+
+#include "folder.h"
+
+namespace OXA
+{
+
+/**
+ * @short A job that requests all folders from the OX server.
+ *
+ * @author Tobias Koenig <tokoe@kde.org>
+ */
+class FoldersRequestJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Describes the mode of the request job.
+     */
+    enum Mode {
+        Modified,  ///< Fetches all new and modified folders
+        Deleted    ///< Fetches all deleted folders
+    };
+
+    /**
+     * Creates a new folders request job.
+     *
+     * @param lastSync The timestamp of the last sync. Only added, modified or deleted folders
+     *                 after this date will be requested. 0 will request all available folders.
+     * @param mode The mode of folders to request.
+     * @param parent The parent object.
+     */
+    explicit FoldersRequestJob(qulonglong lastSync = 0, Mode mode = Modified, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Starts the job.
+     */
+    void start() Q_DECL_OVERRIDE;
+
+    /**
+     * Returns the list of all requested folders.
+     */
+    Folder::List folders() const;
+
+private Q_SLOTS:
+    void davJobFinished(KJob *);
+
+private:
+    qulonglong mLastSync;
+    Mode mMode;
+    Folder::List mFolders;
+};
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/folderutils.cpp b/resources/openxchange/oxa/folderutils.cpp
new file mode 100644 (file)
index 0000000..10527e4
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "folderutils.h"
+
+#include "davutils.h"
+#include "folder.h"
+#include "oxutils.h"
+
+#include <QtXml/QDomElement>
+
+using namespace OXA;
+
+static void createFolderPermissions(const Folder &folder, QDomDocument &document, QDomElement &permissions)
+{
+    {
+        const Folder::UserPermissions userPermissions = folder.userPermissions();
+        Folder::UserPermissions::ConstIterator it = userPermissions.constBegin();
+        while (it != userPermissions.constEnd()) {
+            QDomElement user = DAVUtils::addOxElement(document, permissions, QStringLiteral("user"),
+                               OXUtils::writeNumber(it.key()));
+            DAVUtils::setOxAttribute(user, QStringLiteral("folderpermission"),
+                                     OXUtils::writeNumber(it.value().folderPermission()));
+            DAVUtils::setOxAttribute(user, QStringLiteral("objectreadpermission"),
+                                     OXUtils::writeNumber(it.value().objectReadPermission()));
+            DAVUtils::setOxAttribute(user, QStringLiteral("objectwritepermission"),
+                                     OXUtils::writeNumber(it.value().objectWritePermission()));
+            DAVUtils::setOxAttribute(user, QStringLiteral("objectdeletepermission"),
+                                     OXUtils::writeNumber(it.value().objectDeletePermission()));
+            DAVUtils::setOxAttribute(user, QStringLiteral("admin_flag"),
+                                     OXUtils::writeBoolean(it.value().adminFlag()));
+
+            ++it;
+        }
+    }
+
+    {
+        const Folder::GroupPermissions groupPermissions = folder.groupPermissions();
+        Folder::GroupPermissions::ConstIterator it = groupPermissions.constBegin();
+        while (it != groupPermissions.constEnd()) {
+            QDomElement group = DAVUtils::addOxElement(document, permissions, QStringLiteral("group"),
+                                OXUtils::writeNumber(it.key()));
+            DAVUtils::setOxAttribute(group, QStringLiteral("folderpermission"),
+                                     OXUtils::writeNumber(it.value().folderPermission()));
+            DAVUtils::setOxAttribute(group, QStringLiteral("objectreadpermission"),
+                                     OXUtils::writeNumber(it.value().objectReadPermission()));
+            DAVUtils::setOxAttribute(group, QStringLiteral("objectwritepermission"),
+                                     OXUtils::writeNumber(it.value().objectWritePermission()));
+            DAVUtils::setOxAttribute(group, QStringLiteral("objectdeletepermission"),
+                                     OXUtils::writeNumber(it.value().objectDeletePermission()));
+            DAVUtils::setOxAttribute(group, QStringLiteral("admin_flag"),
+                                     OXUtils::writeBoolean(it.value().adminFlag()));
+
+            ++it;
+        }
+    }
+}
+
+static void parseFolderPermissions(const QDomElement &permissions, Folder &folder)
+{
+    Folder::UserPermissions userPermissions;
+    Folder::GroupPermissions groupPermissions;
+
+    QDomElement element = permissions.firstChildElement();
+    while (!element.isNull()) {
+        if (element.tagName() == QLatin1String("user")) {
+            Folder::Permissions permissions;
+            permissions.setFolderPermission((Folder::Permissions::FolderPermission)OXUtils::readNumber(element.attribute(QStringLiteral("folderpermission"), QStringLiteral("0"))));
+            permissions.setObjectReadPermission((Folder::Permissions::ObjectReadPermission)OXUtils::readNumber(element.attribute(QStringLiteral("objectreadpermission"), QStringLiteral("0"))));
+            permissions.setObjectWritePermission((Folder::Permissions::ObjectWritePermission)OXUtils::readNumber(element.attribute(QStringLiteral("objectwritepermission"), QStringLiteral("0"))));
+            permissions.setObjectDeletePermission((Folder::Permissions::ObjectDeletePermission)OXUtils::readNumber(element.attribute(QStringLiteral("objectdeletepermission"), QStringLiteral("0"))));
+            permissions.setAdminFlag(OXUtils::readBoolean(element.attribute(QStringLiteral("admin_flag"), QStringLiteral("false"))));
+
+            userPermissions.insert(OXUtils::readNumber(element.text()), permissions);
+        } else if (element.tagName() == QLatin1String("group")) {
+            Folder::Permissions permissions;
+            permissions.setFolderPermission((Folder::Permissions::FolderPermission)OXUtils::readNumber(element.attribute(QStringLiteral("folderpermission"), QStringLiteral("0"))));
+            permissions.setObjectReadPermission((Folder::Permissions::ObjectReadPermission)OXUtils::readNumber(element.attribute(QStringLiteral("objectreadpermission"), QStringLiteral("0"))));
+            permissions.setObjectWritePermission((Folder::Permissions::ObjectWritePermission)OXUtils::readNumber(element.attribute(QStringLiteral("objectwritepermission"), QStringLiteral("0"))));
+            permissions.setObjectDeletePermission((Folder::Permissions::ObjectDeletePermission)OXUtils::readNumber(element.attribute(QStringLiteral("objectdeletepermission"), QStringLiteral("0"))));
+            permissions.setAdminFlag(OXUtils::readBoolean(element.attribute(QStringLiteral("admin_flag"), QStringLiteral("false"))));
+
+            groupPermissions.insert(OXUtils::readNumber(element.text()), permissions);
+        }
+
+        element = element.nextSiblingElement();
+    }
+
+    folder.setUserPermissions(userPermissions);
+    folder.setGroupPermissions(groupPermissions);
+}
+
+Folder OXA::FolderUtils::parseFolder(const QDomElement &propElement)
+{
+    Folder folder;
+
+    QDomElement element = propElement.firstChildElement();
+    while (!element.isNull()) {
+        if (element.tagName() == QLatin1String("object_status")) {
+            const QString content = OXUtils::readString(element.text());
+            if (content == QLatin1String("CREATE")) {
+                folder.setObjectStatus(Folder::Created);
+            } else if (content == QLatin1String("DELETE")) {
+                folder.setObjectStatus(Folder::Deleted);
+            } else {
+                Q_ASSERT(false);
+            }
+        } else if (element.tagName() == QLatin1String("title")) {
+            folder.setTitle(OXUtils::readString(element.text()));
+        } else if (element.tagName() == QLatin1String("owner")) {
+            folder.setOwner(OXUtils::readNumber(element.text()));
+        } else if (element.tagName() == QLatin1String("module")) {
+            const QString content = OXUtils::readString(element.text());
+            if (content == QLatin1String("calendar")) {
+                folder.setModule(Folder::Calendar);
+            } else if (content == QLatin1String("contact")) {
+                folder.setModule(Folder::Contacts);
+            } else if (content == QLatin1String("task")) {
+                folder.setModule(Folder::Tasks);
+            } else {
+                folder.setModule(Folder::Unbound);
+            }
+        } else if (element.tagName() == QLatin1String("type")) {
+            const QString content = OXUtils::readString(element.text());
+            if (content == QLatin1String("public")) {
+                folder.setType(Folder::Public);
+            } else if (content == QLatin1String("private")) {
+                folder.setType(Folder::Private);
+            } else {
+                Q_ASSERT(false);
+            }
+        } else if (element.tagName() == QLatin1String("defaultfolder")) {
+            folder.setIsDefaultFolder(OXUtils::readBoolean(element.text()));
+        } else if (element.tagName() == QLatin1String("last_modified")) {
+            folder.setLastModified(OXUtils::readString(element.text()));
+        } else if (element.tagName() == QLatin1String("object_id")) {
+            folder.setObjectId(OXUtils::readNumber(element.text()));
+        } else if (element.tagName() == QLatin1String("folder_id")) {
+            folder.setFolderId(OXUtils::readNumber(element.text()));
+        } else if (element.tagName() == QLatin1String("permissions")) {
+            parseFolderPermissions(element, folder);
+        }
+
+        element = element.nextSiblingElement();
+    }
+
+    return folder;
+}
+
+void OXA::FolderUtils::addFolderElements(QDomDocument &document, QDomElement &propElement, const Folder &folder)
+{
+    DAVUtils::addOxElement(document, propElement, QStringLiteral("title"), OXUtils::writeString(folder.title()));
+    DAVUtils::addOxElement(document, propElement, QStringLiteral("folder_id"), OXUtils::writeNumber(folder.folderId()));
+
+    const QString type = (folder.type() == Folder::Public ? QStringLiteral("public") : QStringLiteral("private"));
+    DAVUtils::addOxElement(document, propElement, QStringLiteral("type"), OXUtils::writeString(type));
+
+    QString module;
+    switch (folder.module()) {
+    case Folder::Calendar: module = QStringLiteral("calendar"); break;
+    case Folder::Contacts: module = QStringLiteral("contact"); break;
+    case Folder::Tasks: module = QStringLiteral("task"); break;
+    default: break;
+    }
+    DAVUtils::addOxElement(document, propElement, QStringLiteral("module"), OXUtils::writeString(module));
+
+    QDomElement permissions = DAVUtils::addOxElement(document, propElement, QStringLiteral("permissions"));
+    createFolderPermissions(folder, document, permissions);
+}
+
diff --git a/resources/openxchange/oxa/folderutils.h b/resources/openxchange/oxa/folderutils.h
new file mode 100644 (file)
index 0000000..1f21d61
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_FOLDERUTILS_H
+#define OXA_FOLDERUTILS_H
+
+class QDomDocument;
+class QDomElement;
+
+namespace OXA
+{
+
+class Folder;
+
+/**
+ * Namespace that contains helper methods for handling folders.
+ *
+ * @author Tobias Koenig <tokoe@kde.org>
+ */
+namespace FolderUtils
+{
+/**
+ * Parses the XML tree under @p propElement and return the folder.
+ */
+Folder parseFolder(const QDomElement &propElement);
+
+/**
+ * Adds the @p folder data to the @p document under the @p propElement.
+ */
+void addFolderElements(QDomDocument &document, QDomElement &propElement, const Folder &folder);
+}
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/incidenceutils.cpp b/resources/openxchange/oxa/incidenceutils.cpp
new file mode 100644 (file)
index 0000000..40e986b
--- /dev/null
@@ -0,0 +1,595 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "incidenceutils.h"
+
+#include "davmanager.h"
+#include "davutils.h"
+#include "oxutils.h"
+#include "users.h"
+
+#include <KCalCore/Event>
+#include <KCalCore/Todo>
+
+#include <QtXml/QDomElement>
+
+#include <QtCore/QBitArray>
+#include <QtCore/QDebug>
+
+using namespace OXA;
+
+static void parseMembersAttribute(const QDomElement &element,
+                                  const KCalCore::Incidence::Ptr &incidence)
+{
+    incidence->clearAttendees();
+
+    for (QDomElement child = element.firstChildElement(); !child.isNull(); child = child.nextSiblingElement()) {
+        if (child.tagName() == QLatin1String("user")) {
+            const QString uid = child.text();
+
+            const User user = Users::self()->lookupUid(uid.toLongLong());
+
+            QString name;
+            QString email;
+            KCalCore::Attendee::Ptr attendee = incidence->attendeeByUid(uid);
+            if (!user.isValid()) {
+                if (attendee) {
+                    continue;
+                }
+
+                name = uid;
+                email = uid + QLatin1Char('@') + DavManager::self()->baseUrl().host();
+            } else {
+                name = user.name();
+                email = user.email();
+            }
+
+            if (attendee) {
+                attendee->setName(name);
+                attendee->setEmail(email);
+            } else {
+                attendee = KCalCore::Attendee::Ptr(new KCalCore::Attendee(name, email));
+                attendee->setUid(uid);
+                incidence->addAttendee(attendee);
+            }
+
+            const QString status = child.attribute(QStringLiteral("confirm"));
+            if (!status.isEmpty()) {
+                if (status == QLatin1String("accept")) {
+                    attendee->setStatus(KCalCore::Attendee::Accepted);
+                } else if (status == QLatin1String("decline")) {
+                    attendee->setStatus(KCalCore::Attendee::Declined);
+                } else {
+                    attendee->setStatus(KCalCore::Attendee::NeedsAction);
+                }
+            }
+        }
+    }
+}
+
+static void parseIncidenceAttribute(const QDomElement &element,
+                                    const KCalCore::Incidence::Ptr &incidence)
+{
+    const QString tagName = element.tagName();
+    const QString text = OXUtils::readString(element.text());
+
+    if (tagName == QLatin1String("title")) {
+        incidence->setSummary(text);
+    } else if (tagName == QLatin1String("note")) {
+        incidence->setDescription(text);
+    } else if (tagName == QLatin1String("alarm")) {
+        const int minutes = OXUtils::readNumber(element.text());
+        if (minutes != 0) {
+            KCalCore::Alarm::List alarms = incidence->alarms();
+            KCalCore::Alarm::Ptr alarm;
+            if (alarms.isEmpty()) {
+                alarm = incidence->newAlarm();
+            } else {
+                alarm = alarms.first();
+            }
+
+            if (alarm->type() == KCalCore::Alarm::Invalid) {
+                alarm->setType(KCalCore::Alarm::Display);
+            }
+
+            KCalCore::Duration duration(minutes * -60);
+            alarm->setStartOffset(duration);
+            alarm->setEnabled(true);
+        } else {
+            // 0 reminder -> disable alarm
+            incidence->clearAlarms();
+        }
+    } else if (tagName == QLatin1String("created_by")) {
+        const User user = Users::self()->lookupUid(OXUtils::readNumber(element.text()));
+        incidence->setOrganizer(KCalCore::Person::Ptr(new KCalCore::Person(user.name(), user.email())));
+    } else if (tagName == QLatin1String("participants")) {
+        parseMembersAttribute(element, incidence);
+    } else if (tagName == QLatin1String("private_flag")) {
+        if (OXUtils::readBoolean(element.text()) == true) {
+            incidence->setSecrecy(KCalCore::Incidence::SecrecyPrivate);
+        } else {
+            incidence->setSecrecy(KCalCore::Incidence::SecrecyPublic);
+        }
+    } else if (tagName == QLatin1String("categories")) {
+        incidence->setCategories(text.split(QRegExp(QLatin1String(",\\s*"))));
+    }
+}
+
+static void parseEventAttribute(const QDomElement &element,
+                                const KCalCore::Event::Ptr &event)
+{
+    const QString tagName = element.tagName();
+    const QString text = OXUtils::readString(element.text());
+
+    if (tagName == QLatin1String("start_date")) {
+        KDateTime dateTime = KDateTime(OXUtils::readDateTime(element.text()), KDateTime::UTC);
+        if (event->allDay()) {
+            dateTime.setDateOnly(true);
+        }
+
+        event->setDtStart(dateTime);
+
+    } else if (tagName == QLatin1String("end_date")) {
+        KDateTime dateTime = KDateTime(OXUtils::readDateTime(element.text()), KDateTime::UTC);
+        if (event->allDay()) {
+            dateTime = dateTime.addSecs(-1);
+        }
+
+        event->setDtEnd(dateTime);
+
+    } else if (tagName == QLatin1String("location")) {
+        event->setLocation(text);
+    }
+}
+
+static void parseTodoAttribute(const QDomElement &element,
+                               const KCalCore::Todo::Ptr &todo)
+{
+    const QString tagName = element.tagName();
+    const QString text = OXUtils::readString(element.text());
+
+    if (tagName == QLatin1String("start_date")) {
+        const KDateTime dateTime = KDateTime(OXUtils::readDateTime(element.text()), KDateTime::UTC);
+        if (dateTime.isValid()) {
+            todo->setDtStart(dateTime);
+        }
+    } else if (tagName == QLatin1String("end_date")) {
+        const KDateTime dateTime = KDateTime(OXUtils::readDateTime(element.text()), KDateTime::UTC);
+        if (dateTime.isValid()) {
+            todo->setDtDue(dateTime);
+        }
+    } else if (tagName == QLatin1String("priority")) {
+        const int priorityNumber = OXUtils::readNumber(element.text());
+        if (priorityNumber < 1 || priorityNumber > 3) {
+            qDebug() << "Unknown priority:" << text;
+        } else {
+            int priority = 0;
+            switch (priorityNumber) {
+            case 1:
+                priority = 9;
+                break;
+            case 2:
+                priority = 5;
+                break;
+            case 3:
+                priority = 1;
+                break;
+            }
+            todo->setPriority(priority);
+        }
+    } else if (tagName == QLatin1String("percent_completed")) {
+        todo->setPercentComplete(OXUtils::readNumber(element.text()));
+    }
+}
+
+static void parseRecurrence(const QDomElement &element,
+                            const KCalCore::Incidence::Ptr &incidence)
+{
+    QString type;
+
+    int dailyValue = -1;
+    QDateTime endDate;
+
+    int weeklyValue = -1;
+    QBitArray days(7);   // days, starting with monday
+    bool daysSet = false;
+
+    int monthlyValueDay = -1;
+    int monthlyValueMonth = -1;
+
+    int yearlyValueDay = -1;
+    int yearlyMonth = -1;
+
+    int monthly2Recurrency = 0;
+    int monthly2ValueMonth = -1;
+
+    int yearly2Recurrency = 0;
+    int yearly2Day = 0;
+    int yearly2Month = -1;
+
+    KCalCore::DateList deleteExceptions;
+
+    for (QDomElement child = element.firstChildElement(); !child.isNull(); child = child.nextSiblingElement()) {
+        const QString tagName = child.tagName();
+        const QString text = OXUtils::readString(child.text());
+
+        if (tagName == QLatin1String("recurrence_type")) {
+            type = text;
+        } else if (tagName == QLatin1String("interval")) {
+            dailyValue = text.toInt();
+            weeklyValue = text.toInt();
+            monthlyValueMonth = text.toInt();
+            monthly2ValueMonth = text.toInt();
+        } else if (tagName == QLatin1String("days")) {
+            int tmp = text.toInt();  // OX encodes days binary: 1=Su, 2=Mo, 4=Tu, ...
+            for (int i = 0; i < 7; ++i) {
+                if (tmp & (1 << i)) {
+                    days.setBit((i + 6) % 7);
+                }
+            }
+            daysSet = true;
+        } else if (tagName == QLatin1String("day_in_month")) {
+            monthlyValueDay = text.toInt();
+            monthly2Recurrency = text.toInt();
+            yearlyValueDay = text.toInt();
+            yearly2Recurrency = text.toInt();
+        } else if (tagName == QLatin1String("month")) {
+            yearlyMonth = text.toInt() + 1; // starts at 0
+            yearly2Month = text.toInt() + 1;
+        } else if ((tagName == QLatin1String("deleteexceptions")) || (tagName == QLatin1String("changeexceptions"))) {
+            const QStringList exceptionDates = text.split(QStringLiteral(","));
+            deleteExceptions.reserve(exceptionDates.count());
+            foreach (const QString &date, exceptionDates) {
+                deleteExceptions.append(OXUtils::readDate(date));
+            }
+        } else if (tagName == QLatin1String("until")) {
+            endDate = OXUtils::readDateTime(child.text());
+        }
+        //TODO: notification
+    }
+
+    if (daysSet && type == QLatin1String("monthly")) {
+        type = QStringLiteral("monthly2");    // HACK: OX doesn't cleanly distinguish between monthly and monthly2
+    }
+    if (daysSet && type == QLatin1String("yearly")) {
+        type = QStringLiteral("yearly2");
+    }
+
+    KCalCore::Recurrence *recurrence = incidence->recurrence();
+
+    if (type == QLatin1String("daily")) {
+        recurrence->setDaily(dailyValue);
+    } else if (type == QLatin1String("weekly")) {
+        recurrence->setWeekly(weeklyValue, days);
+    } else if (type == QLatin1String("monthly")) {
+        recurrence->setMonthly(monthlyValueMonth);
+        recurrence->addMonthlyDate(monthlyValueDay);
+    } else if (type == QLatin1String("yearly")) {
+        recurrence->setYearly(1);
+        recurrence->addYearlyDate(yearlyValueDay);
+        recurrence->addYearlyMonth(yearlyMonth);
+    } else if (type == QLatin1String("monthly2")) {
+        recurrence->setMonthly(monthly2ValueMonth);
+        QBitArray _days(7);
+        if (daysSet) {
+            _days = days;
+        } else {
+            _days.setBit(incidence->dtStart().date().dayOfWeek());
+        }
+        recurrence->addMonthlyPos(monthly2Recurrency, _days);
+    } else if (type == QLatin1String("yearly2")) {
+        recurrence->setYearly(1);
+        recurrence->addYearlyMonth(yearly2Month);
+        QBitArray _days(7);
+        if (daysSet) {
+            _days = days;
+        } else {
+            _days.setBit((yearly2Day + 5) % 7);
+        }
+        recurrence->addYearlyPos(yearly2Recurrency, _days);
+    }
+
+    if (endDate.isValid()) {
+        recurrence->setEndDate(endDate.date());
+    }
+
+    recurrence->setExDates(deleteExceptions);
+}
+
+static void createIncidenceAttributes(QDomDocument &document, QDomElement &parent,
+                                      const KCalCore::Incidence::Ptr &incidence)
+{
+    DAVUtils::addOxElement(document, parent, QStringLiteral("title"), OXUtils::writeString(incidence->summary()));
+    DAVUtils::addOxElement(document, parent, QStringLiteral("note"), OXUtils::writeString(incidence->description()));
+
+    if (incidence->attendeeCount() > 0) {
+        QDomElement members = DAVUtils::addOxElement(document, parent, QStringLiteral("participants"));
+        const KCalCore::Attendee::List attendees = incidence->attendees();
+        foreach (const KCalCore::Attendee::Ptr &attendee, attendees) {
+            const User user = Users::self()->lookupEmail(attendee->email());
+
+            if (!user.isValid()) {
+                continue;
+            }
+
+            QString status;
+            switch (attendee->status()) {
+            case KCalCore::Attendee::Accepted: status = QStringLiteral("accept"); break;
+            case KCalCore::Attendee::Declined: status = QStringLiteral("decline"); break;
+            default: status = QStringLiteral("none"); break;
+            }
+
+            QDomElement element = DAVUtils::addOxElement(document, members, QStringLiteral("user"), OXUtils::writeNumber(user.uid()));
+            DAVUtils::setOxAttribute(element, QStringLiteral("confirm"), status);
+        }
+    }
+
+    if (incidence->secrecy() == KCalCore::Incidence::SecrecyPublic) {
+        DAVUtils::addOxElement(document, parent, QStringLiteral("private_flag"), OXUtils::writeBoolean(false));
+    } else {
+        DAVUtils::addOxElement(document, parent, QStringLiteral("private_flag"), OXUtils::writeBoolean(true));
+    }
+
+    // set reminder as the number of minutes to the start of the event
+    const KCalCore::Alarm::List alarms = incidence->alarms();
+    if (!alarms.isEmpty() && alarms.first()->hasStartOffset() && alarms.first()->enabled()) {
+        DAVUtils::addOxElement(document, parent, QStringLiteral("alarm_flag"), OXUtils::writeBoolean(true));
+        DAVUtils::addOxElement(document, parent, QStringLiteral("alarm"), OXUtils::writeNumber((-1) * alarms.first()->startOffset().asSeconds() / 60));
+    } else {
+        DAVUtils::addOxElement(document, parent, QStringLiteral("alarm_flag"), OXUtils::writeBoolean(false));
+        DAVUtils::addOxElement(document, parent, QStringLiteral("alarm"), QStringLiteral("0"));
+    }
+
+    // categories
+    DAVUtils::addOxElement(document, parent, QStringLiteral("categories"), OXUtils::writeString(incidence->categories().join(QStringLiteral(", "))));
+}
+
+static void createEventAttributes(QDomDocument &document, QDomElement &parent,
+                                  const KCalCore::Event::Ptr &event)
+{
+    if (event->allDay()) {
+        DAVUtils::addOxElement(document, parent, QStringLiteral("start_date"), OXUtils::writeDate(event->dtStart().date()));
+        if (event->hasEndDate()) {
+            DAVUtils::addOxElement(document, parent, QStringLiteral("end_date"), OXUtils::writeDate(event->dtEnd().date()));
+        }
+    } else {
+        DAVUtils::addOxElement(document, parent, QStringLiteral("start_date"), OXUtils::writeDateTime(event->dtStart().dateTime()));
+        if (event->hasEndDate()) {
+            DAVUtils::addOxElement(document, parent, QStringLiteral("end_date"), OXUtils::writeDateTime(event->dtEnd().dateTime()));
+        }
+    }
+
+    if (!event->hasEndDate()) {
+        DAVUtils::addOxElement(document, parent, QStringLiteral("end_date"));
+    }
+
+    DAVUtils::addOxElement(document, parent, QStringLiteral("location"), OXUtils::writeString(event->location()));
+    DAVUtils::addOxElement(document, parent, QStringLiteral("full_time"), OXUtils::writeBoolean(event->allDay()));
+
+    if (event->transparency() == KCalCore::Event::Transparent) {
+        DAVUtils::addOxElement(document, parent, QStringLiteral("shown_as"), OXUtils::writeNumber(4));
+    } else if (event->transparency() == KCalCore::Event::Opaque) {
+        DAVUtils::addOxElement(document, parent, QStringLiteral("shown_as"), OXUtils::writeNumber(1));
+    }
+
+}
+
+static void createTaskAttributes(QDomDocument &document, QDomElement &parent,
+                                 const KCalCore::Todo::Ptr &todo)
+{
+    if (todo->hasStartDate()) {
+        DAVUtils::addOxElement(document, parent, QStringLiteral("start_date"), OXUtils::writeDateTime(todo->dtStart().dateTime()));
+    } else {
+        DAVUtils::addOxElement(document, parent, QStringLiteral("start_date"));
+    }
+
+    if (todo->hasDueDate()) {
+        DAVUtils::addOxElement(document, parent, QStringLiteral("end_date"), OXUtils::writeDateTime(todo->dtDue().dateTime()));
+    } else {
+        DAVUtils::addOxElement(document, parent, QStringLiteral("end_date"));
+    }
+
+    QString priority;
+    switch (todo->priority()) {
+    case 9:
+    case 8:
+        priority = QStringLiteral("1");
+        break;
+    case 2:
+    case 1:
+        priority = QStringLiteral("3");
+        break;
+    default:
+        priority = QStringLiteral("2");
+        break;
+    }
+    DAVUtils::addOxElement(document, parent, QStringLiteral("priority"), priority);
+
+    DAVUtils::addOxElement(document, parent, QStringLiteral("percent_completed"), OXUtils::writeNumber(todo->percentComplete()));
+}
+
+static void createRecurrenceAttributes(QDomDocument &document, QDomElement &parent,
+                                       const KCalCore::Incidence::Ptr &incidence)
+{
+    if (!incidence->recurs()) {
+        DAVUtils::addOxElement(document, parent, QStringLiteral("recurrence_type"), QStringLiteral("none"));
+        return;
+    }
+
+    const KCalCore::Recurrence *recurrence = incidence->recurrence();
+    int monthOffset = -1;
+    switch (recurrence->recurrenceType()) {
+    case KCalCore::Recurrence::rDaily:
+        DAVUtils::addOxElement(document, parent, QStringLiteral("recurrence_type"), QStringLiteral("daily"));
+        DAVUtils::addOxElement(document, parent, QStringLiteral("interval"), OXUtils::writeNumber(recurrence->frequency()));
+        break;
+    case KCalCore::Recurrence::rWeekly: {
+        DAVUtils::addOxElement(document, parent, QStringLiteral("recurrence_type"), QStringLiteral("weekly"));
+        DAVUtils::addOxElement(document, parent, QStringLiteral("interval"), OXUtils::writeNumber(recurrence->frequency()));
+
+        int days = 0;
+        for (int i = 0; i < 7; ++i) {
+            if (recurrence->days()[i]) {
+                days += 1 << ((i + 1) % 7);
+            }
+        }
+
+        DAVUtils::addOxElement(document, parent, QStringLiteral("days"), OXUtils::writeNumber(days));
+    }
+    break;
+    case KCalCore::Recurrence::rMonthlyDay:
+        DAVUtils::addOxElement(document, parent, QStringLiteral("recurrence_type"), QStringLiteral("monthly"));
+        DAVUtils::addOxElement(document, parent, QStringLiteral("interval"), OXUtils::writeNumber(recurrence->frequency()));
+        DAVUtils::addOxElement(document, parent, QStringLiteral("day_in_month"), OXUtils::writeNumber(recurrence->monthDays().first()));
+        break;
+    case KCalCore::Recurrence::rMonthlyPos: {
+        const KCalCore::RecurrenceRule::WDayPos wdp = recurrence->monthPositions().first();
+
+        DAVUtils::addOxElement(document, parent, QStringLiteral("recurrence_type"), QStringLiteral("monthly"));
+        DAVUtils::addOxElement(document, parent, QStringLiteral("interval"), OXUtils::writeNumber(recurrence->frequency()));
+        DAVUtils::addOxElement(document, parent, QStringLiteral("days"), OXUtils::writeNumber(1 << wdp.day()));
+        DAVUtils::addOxElement(document, parent, QStringLiteral("day_in_month"), OXUtils::writeNumber(wdp.pos()));
+    }
+    break;
+    case KCalCore::Recurrence::rYearlyMonth:
+        DAVUtils::addOxElement(document, parent, QStringLiteral("recurrence_type"), QStringLiteral("yearly"));
+        DAVUtils::addOxElement(document, parent, QStringLiteral("interval"), QStringLiteral("1"));
+        DAVUtils::addOxElement(document, parent, QStringLiteral("day_in_month"), OXUtils::writeNumber(recurrence->yearDates().first()));
+        DAVUtils::addOxElement(document, parent, QStringLiteral("month"), OXUtils::writeNumber(recurrence->yearMonths().first() + monthOffset));
+        break;
+    case KCalCore::Recurrence::rYearlyPos: {
+        const KCalCore::RecurrenceRule::WDayPos wdp = recurrence->monthPositions().first();
+
+        DAVUtils::addOxElement(document, parent, QStringLiteral("recurrence_type"), QStringLiteral("yearly"));
+        DAVUtils::addOxElement(document, parent, QStringLiteral("interval"), QStringLiteral("1"));
+        DAVUtils::addOxElement(document, parent, QStringLiteral("days"), OXUtils::writeNumber(1 << wdp.day()));
+        DAVUtils::addOxElement(document, parent, QStringLiteral("day_in_month"), OXUtils::writeNumber(wdp.pos()));
+        DAVUtils::addOxElement(document, parent, QStringLiteral("month"), OXUtils::writeNumber(recurrence->yearMonths().first() + monthOffset));
+    }
+    break;
+    default:
+        qDebug() << "unsupported recurrence type:" << recurrence->recurrenceType();
+    }
+
+    if (recurrence->endDateTime().isValid()) {
+        DAVUtils::addOxElement(document, parent, QStringLiteral("until"), OXUtils::writeDateTime(recurrence->endDateTime().dateTime()));
+    } else {
+        DAVUtils::addOxElement(document, parent, QStringLiteral("until"));
+    }
+
+    // delete exceptions
+    const KCalCore::DateList exceptionList = recurrence->exDates();
+
+    QStringList dates;
+    dates.reserve(exceptionList.count());
+    foreach (const QDate &date, exceptionList) {
+        dates.append(OXUtils::writeDate(date));
+    }
+
+    DAVUtils::addOxElement(document, parent, QStringLiteral("deleteexceptions"), dates.join(QStringLiteral(",")));
+
+    //TODO: changeexceptions
+
+}
+
+void OXA::IncidenceUtils::parseEvent(const QDomElement &propElement, Object &object)
+{
+    KCalCore::Event::Ptr event(new KCalCore::Event);
+
+    const QDomElement fullTimeElement = propElement.firstChildElement(QStringLiteral("full_time"));
+    if (!fullTimeElement.isNull()) {
+        event->setAllDay(OXUtils::readBoolean(fullTimeElement.text()));
+    }
+
+    const QDomElement ShowAsElement = propElement.firstChildElement(QStringLiteral("shown_as"));
+    if (!ShowAsElement.isNull()) {
+        int showAs = OXUtils::readNumber(ShowAsElement.text());
+        switch (showAs) {
+        case 1 : event->setTransparency(KCalCore::Event::Transparent); break;
+        case 4 : event->setTransparency(KCalCore::Event::Opaque); break;
+        default : event->setTransparency(KCalCore::Event::Opaque);
+        }
+    }
+
+    bool doesRecur = false;
+    const QDomElement recurrenceTypeElement = propElement.firstChildElement(QStringLiteral("recurrence_type"));
+    if (!recurrenceTypeElement.isNull() && recurrenceTypeElement.text() != QLatin1String("none")) {
+        doesRecur = true;
+    }
+
+    QDomElement element = propElement.firstChildElement();
+    while (!element.isNull()) {
+        parseIncidenceAttribute(element, event);
+        parseEventAttribute(element, event);
+
+        element = element.nextSiblingElement();
+    }
+
+    if (doesRecur) {
+        parseRecurrence(propElement, event);
+    } else {
+        event->recurrence()->unsetRecurs();
+    }
+
+    object.setEvent(KCalCore::Incidence::Ptr(event));
+}
+
+void OXA::IncidenceUtils::parseTask(const QDomElement &propElement, Object &object)
+{
+    KCalCore::Todo::Ptr todo(new KCalCore::Todo);
+    todo->setSecrecy(KCalCore::Incidence::SecrecyPrivate);
+
+    bool doesRecur = false;
+    const QDomElement recurrenceTypeElement = propElement.firstChildElement(QStringLiteral("recurrence_type"));
+    if (!recurrenceTypeElement.isNull() && recurrenceTypeElement.text() != QLatin1String("none")) {
+        doesRecur = true;
+    }
+
+    QDomElement element = propElement.firstChildElement();
+    while (!element.isNull()) {
+        parseIncidenceAttribute(element, todo);
+        parseTodoAttribute(element, todo);
+
+        element = element.nextSiblingElement();
+    }
+
+    if (doesRecur) {
+        parseRecurrence(propElement, todo);
+    } else {
+        todo->recurrence()->unsetRecurs();
+    }
+
+    object.setTask(KCalCore::Incidence::Ptr(todo));
+}
+
+void OXA::IncidenceUtils::addEventElements(QDomDocument &document, QDomElement &propElement, const Object &object)
+{
+    createIncidenceAttributes(document, propElement, object.event());
+    createEventAttributes(document, propElement, object.event().staticCast<KCalCore::Event>());
+    createRecurrenceAttributes(document, propElement, object.event());
+}
+
+void OXA::IncidenceUtils::addTaskElements(QDomDocument &document, QDomElement &propElement, const Object &object)
+{
+    createIncidenceAttributes(document, propElement, object.task());
+    createTaskAttributes(document, propElement, object.task().staticCast<KCalCore::Todo>());
+    createRecurrenceAttributes(document, propElement, object.task());
+}
diff --git a/resources/openxchange/oxa/incidenceutils.h b/resources/openxchange/oxa/incidenceutils.h
new file mode 100644 (file)
index 0000000..950c5d3
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_INCIDENCEUTILS_H
+#define OXA_INCIDENCEUTILS_H
+
+#include "object.h"
+
+class QDomDocument;
+class QDomElement;
+
+namespace OXA
+{
+
+/**
+ * Namespace that contains helper methods for handling events and tasks.
+ */
+namespace IncidenceUtils
+{
+/**
+ * Parses the XML tree under @p propElement and fills the event data of @p object.
+ */
+void parseEvent(const QDomElement &propElement, Object &object);
+
+/**
+ * Parses the XML tree under @p propElement and fills the task data of @p object.
+ */
+void parseTask(const QDomElement &propElement, Object &object);
+
+/**
+ * Adds the event data of @p object to the @p document under the @p propElement.
+ */
+void addEventElements(QDomDocument &document, QDomElement &propElement, const Object &object);
+
+/**
+ * Adds the task data of @p object to the @p document under the @p propElement.
+ */
+void addTaskElements(QDomDocument &document, QDomElement &propElement, const Object &object);
+}
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/object.cpp b/resources/openxchange/oxa/object.cpp
new file mode 100644 (file)
index 0000000..b4a9d01
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "object.h"
+
+using namespace OXA;
+
+Object::Object()
+    : mObjectStatus(Created), mObjectId(-1), mFolderId(-1)
+{
+}
+
+void Object::setObjectStatus(ObjectStatus status)
+{
+    mObjectStatus = status;
+}
+
+Object::ObjectStatus Object::objectStatus() const
+{
+    return mObjectStatus;
+}
+
+void Object::setObjectId(qlonglong id)
+{
+    mObjectId = id;
+}
+
+qlonglong Object::objectId() const
+{
+    return mObjectId;
+}
+
+void Object::setFolderId(qlonglong id)
+{
+    mFolderId = id;
+}
+
+qlonglong Object::folderId() const
+{
+    return mFolderId;
+}
+
+void Object::setLastModified(const QString &timeStamp)
+{
+    mLastModified = timeStamp;
+}
+
+QString Object::lastModified() const
+{
+    return mLastModified;
+}
+
+void Object::setModule(Folder::Module module)
+{
+    mModule = module;
+}
+
+Folder::Module Object::module() const
+{
+    return mModule;
+}
+
+void Object::setContact(const KContacts::Addressee &contact)
+{
+    mModule = Folder::Contacts;
+    mContact = contact;
+}
+
+KContacts::Addressee Object::contact() const
+{
+    return mContact;
+}
+
+void Object::setContactGroup(const KContacts::ContactGroup &group)
+{
+    mModule = Folder::Contacts;
+    mContactGroup = group;
+}
+
+KContacts::ContactGroup Object::contactGroup() const
+{
+    return mContactGroup;
+}
+
+void Object::setEvent(const KCalCore::Incidence::Ptr &event)
+{
+    mModule = Folder::Calendar;
+    mEvent = event;
+}
+
+KCalCore::Incidence::Ptr Object::event() const
+{
+    return mEvent;
+}
+
+void Object::setTask(const KCalCore::Incidence::Ptr &task)
+{
+    mModule = Folder::Tasks;
+    mTask = task;
+}
+
+KCalCore::Incidence::Ptr Object::task() const
+{
+    return mTask;
+}
diff --git a/resources/openxchange/oxa/object.h b/resources/openxchange/oxa/object.h
new file mode 100644 (file)
index 0000000..3035a49
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_OBJECT_H
+#define OXA_OBJECT_H
+
+#include "folder.h"
+
+#include <kcontacts/addressee.h>
+#include <kcontacts/contactgroup.h>
+#include <KCalCore/Incidence>
+
+#include <QtCore/QVector>
+#include <QtCore/QString>
+
+namespace OXA
+{
+
+class Object
+{
+public:
+    /**
+     * Describes a list of objects.
+     */
+    typedef QVector<Object> List;
+
+    /**
+     * Describes the status of the object.
+     */
+    enum ObjectStatus {
+        Created, ///< The object has been created or modified.
+        Deleted  ///< The object has been deleted.
+    };
+
+    Object();
+
+    void setObjectStatus(ObjectStatus status);
+    ObjectStatus objectStatus() const;
+
+    void setObjectId(qlonglong id);
+    qlonglong objectId() const;
+
+    void setFolderId(qlonglong id);
+    qlonglong folderId() const;
+
+    void setLastModified(const QString &timeStamp);
+    QString lastModified() const;
+
+    void setModule(Folder::Module module);
+    Folder::Module module() const;
+
+    void setContact(const KContacts::Addressee &contact);
+    KContacts::Addressee contact() const;
+
+    void setContactGroup(const KContacts::ContactGroup &group);
+    KContacts::ContactGroup contactGroup() const;
+
+    void setEvent(const KCalCore::Incidence::Ptr &event);
+    KCalCore::Incidence::Ptr event() const;
+
+    void setTask(const KCalCore::Incidence::Ptr &task);
+    KCalCore::Incidence::Ptr task() const;
+
+private:
+    ObjectStatus mObjectStatus;
+    qlonglong mObjectId;
+    qlonglong mFolderId;
+    QString mLastModified;
+    Folder::Module mModule;
+    KContacts::Addressee mContact;
+    KContacts::ContactGroup mContactGroup;
+    KCalCore::Incidence::Ptr mEvent;
+    KCalCore::Incidence::Ptr mTask;
+};
+}
+
+Q_DECLARE_TYPEINFO(OXA::Object, Q_MOVABLE_TYPE);
+
+#endif
diff --git a/resources/openxchange/oxa/objectcreatejob.cpp b/resources/openxchange/oxa/objectcreatejob.cpp
new file mode 100644 (file)
index 0000000..a89673f
--- /dev/null
@@ -0,0 +1,120 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "objectcreatejob.h"
+
+#include "davmanager.h"
+#include "davutils.h"
+#include "objectutils.h"
+#include "oxutils.h"
+
+#include <kio/davjob.h>
+
+using namespace OXA;
+
+ObjectCreateJob::ObjectCreateJob(const Object &object, QObject *parent)
+    : KJob(parent), mObject(object)
+{
+}
+
+void ObjectCreateJob::start()
+{
+    if (ObjectUtils::needsPreloading(mObject)) {
+        KJob *job = ObjectUtils::preloadJob(mObject);
+        connect(job, &KJob::result, this, &ObjectCreateJob::preloadingJobFinished);
+        job->start();
+    } else {
+        QDomDocument document;
+        QDomElement propertyupdate = DAVUtils::addDavElement(document, document, QStringLiteral("propertyupdate"));
+        QDomElement set = DAVUtils::addDavElement(document, propertyupdate, QStringLiteral("set"));
+        QDomElement prop = DAVUtils::addDavElement(document, set, QStringLiteral("prop"));
+
+        ObjectUtils::addObjectElements(document, prop, mObject);
+
+        const QString path = ObjectUtils::davPath(mObject.module());
+
+        KIO::DavJob *job = DavManager::self()->createPatchJob(path, document);
+        connect(job, &KJob::result, this, &ObjectCreateJob::davJobFinished);
+    }
+}
+
+Object ObjectCreateJob::object() const
+{
+    return mObject;
+}
+
+void ObjectCreateJob::preloadingJobFinished(KJob *job)
+{
+    void *preloadedData = ObjectUtils::preloadData(mObject, job);
+
+    QDomDocument document;
+    QDomElement propertyupdate = DAVUtils::addDavElement(document, document, QStringLiteral("propertyupdate"));
+    QDomElement set = DAVUtils::addDavElement(document, propertyupdate, QStringLiteral("set"));
+    QDomElement prop = DAVUtils::addDavElement(document, set, QStringLiteral("prop"));
+
+    ObjectUtils::addObjectElements(document, prop, mObject, preloadedData);
+
+    const QString path = ObjectUtils::davPath(mObject.module());
+
+    KIO::DavJob *davJob = DavManager::self()->createPatchJob(path, document);
+    connect(davJob, &KIO::DavJob::result, this, &ObjectCreateJob::davJobFinished);
+}
+
+void ObjectCreateJob::davJobFinished(KJob *job)
+{
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+        emitResult();
+        return;
+    }
+
+    KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
+
+    const QDomDocument document = davJob->response();
+
+    QString errorText, errorStatus;
+    if (DAVUtils::davErrorOccurred(document, errorText, errorStatus)) {
+        setError(UserDefinedError);
+        setErrorText(errorText);
+        emitResult();
+        return;
+    }
+
+    QDomElement multistatus = document.documentElement();
+    QDomElement response = multistatus.firstChildElement(QStringLiteral("response"));
+    const QDomNodeList props = response.elementsByTagName(QStringLiteral("prop"));
+    const QDomElement prop = props.at(0).toElement();
+
+    QDomElement element = prop.firstChildElement();
+    while (!element.isNull()) {
+        if (element.tagName() == QLatin1String("object_id")) {
+            mObject.setObjectId(OXUtils::readNumber(element.text()));
+        } else if (element.tagName() == QLatin1String("last_modified")) {
+            mObject.setLastModified(OXUtils::readString(element.text()));
+        }
+
+        element = element.nextSiblingElement();
+    }
+
+    emitResult();
+}
+
diff --git a/resources/openxchange/oxa/objectcreatejob.h b/resources/openxchange/oxa/objectcreatejob.h
new file mode 100644 (file)
index 0000000..74a6572
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_OBJECTCREATEJOB_H
+#define OXA_OBJECTCREATEJOB_H
+
+#include <kjob.h>
+
+#include "object.h"
+
+namespace OXA
+{
+
+class ObjectCreateJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    explicit ObjectCreateJob(const Object &object, QObject *parent = Q_NULLPTR);
+
+    void start() Q_DECL_OVERRIDE;
+
+    Object object() const;
+
+private Q_SLOTS:
+    void preloadingJobFinished(KJob *);
+    void davJobFinished(KJob *);
+
+private:
+    Object mObject;
+};
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/objectdeletejob.cpp b/resources/openxchange/oxa/objectdeletejob.cpp
new file mode 100644 (file)
index 0000000..ad0dd8b
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "objectdeletejob.h"
+
+#include "davmanager.h"
+#include "davutils.h"
+#include "objectutils.h"
+#include "oxutils.h"
+
+#include <kio/davjob.h>
+
+using namespace OXA;
+
+ObjectDeleteJob::ObjectDeleteJob(const Object &object, QObject *parent)
+    : KJob(parent), mObject(object)
+{
+}
+
+void ObjectDeleteJob::start()
+{
+    QDomDocument document;
+    QDomElement propertyupdate = DAVUtils::addDavElement(document, document, QStringLiteral("propertyupdate"));
+    QDomElement set = DAVUtils::addDavElement(document, propertyupdate, QStringLiteral("set"));
+    QDomElement prop = DAVUtils::addDavElement(document, set, QStringLiteral("prop"));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("object_id"), OXUtils::writeNumber(mObject.objectId()));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("folder_id"), OXUtils::writeNumber(mObject.folderId()));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("method"), OXUtils::writeString(QStringLiteral("DELETE")));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("last_modified"), OXUtils::writeString(mObject.lastModified()));
+
+    const QString path = ObjectUtils::davPath(mObject.module());
+
+    KIO::DavJob *job = DavManager::self()->createPatchJob(path, document);
+    connect(job, &KIO::DavJob::result, this, &ObjectDeleteJob::davJobFinished);
+}
+
+void ObjectDeleteJob::davJobFinished(KJob *job)
+{
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+        emitResult();
+        return;
+    }
+
+    KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
+
+    const QDomDocument document = davJob->response();
+
+    QString errorText, errorStatus;
+    if (DAVUtils::davErrorOccurred(document, errorText, errorStatus)) {
+        setError(UserDefinedError);
+        setErrorText(errorText);
+        emitResult();
+        return;
+    }
+
+    emitResult();
+}
+
diff --git a/resources/openxchange/oxa/objectdeletejob.h b/resources/openxchange/oxa/objectdeletejob.h
new file mode 100644 (file)
index 0000000..ba5abf4
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_OBJECTDELETEJOB_H
+#define OXA_OBJECTDELETEJOB_H
+
+#include "object.h"
+
+#include <kjob.h>
+
+namespace OXA
+{
+
+class ObjectDeleteJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    explicit ObjectDeleteJob(const Object &object, QObject *parent = Q_NULLPTR);
+
+    void start() Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void davJobFinished(KJob *);
+
+private:
+    Object mObject;
+};
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/objectmodifyjob.cpp b/resources/openxchange/oxa/objectmodifyjob.cpp
new file mode 100644 (file)
index 0000000..2bc72de
--- /dev/null
@@ -0,0 +1,118 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "objectmodifyjob.h"
+
+#include "davmanager.h"
+#include "davutils.h"
+#include "objectutils.h"
+#include "oxutils.h"
+
+#include <kio/davjob.h>
+
+using namespace OXA;
+
+ObjectModifyJob::ObjectModifyJob(const Object &object, QObject *parent)
+    : KJob(parent), mObject(object)
+{
+}
+
+void ObjectModifyJob::start()
+{
+    if (ObjectUtils::needsPreloading(mObject)) {
+        KJob *job = ObjectUtils::preloadJob(mObject);
+        connect(job, &KJob::result, this, &ObjectModifyJob::preloadingJobFinished);
+        job->start();
+    } else {
+        QDomDocument document;
+        QDomElement propertyupdate = DAVUtils::addDavElement(document, document, QStringLiteral("propertyupdate"));
+        QDomElement set = DAVUtils::addDavElement(document, propertyupdate, QStringLiteral("set"));
+        QDomElement prop = DAVUtils::addDavElement(document, set, QStringLiteral("prop"));
+
+        ObjectUtils::addObjectElements(document, prop, mObject);
+
+        const QString path = ObjectUtils::davPath(mObject.module());
+
+        KIO::DavJob *job = DavManager::self()->createPatchJob(path, document);
+        connect(job, &KJob::result, this, &ObjectModifyJob::davJobFinished);
+    }
+}
+
+Object ObjectModifyJob::object() const
+{
+    return mObject;
+}
+
+void ObjectModifyJob::preloadingJobFinished(KJob *job)
+{
+    void *preloadedData = ObjectUtils::preloadData(mObject, job);
+
+    QDomDocument document;
+    QDomElement propertyupdate = DAVUtils::addDavElement(document, document, QStringLiteral("propertyupdate"));
+    QDomElement set = DAVUtils::addDavElement(document, propertyupdate, QStringLiteral("set"));
+    QDomElement prop = DAVUtils::addDavElement(document, set, QStringLiteral("prop"));
+
+    ObjectUtils::addObjectElements(document, prop, mObject, preloadedData);
+
+    const QString path = ObjectUtils::davPath(mObject.module());
+
+    KIO::DavJob *davJob = DavManager::self()->createPatchJob(path, document);
+    connect(davJob, &KIO::DavJob::result, this, &ObjectModifyJob::davJobFinished);
+}
+
+void ObjectModifyJob::davJobFinished(KJob *job)
+{
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+        emitResult();
+        return;
+    }
+
+    KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
+
+    const QDomDocument document = davJob->response();
+
+    QString errorText, errorStatus;
+    if (DAVUtils::davErrorOccurred(document, errorText, errorStatus)) {
+        setError(UserDefinedError);
+        setErrorText(errorText);
+        emitResult();
+        return;
+    }
+
+    QDomElement multistatus = document.documentElement();
+    QDomElement response = multistatus.firstChildElement(QStringLiteral("response"));
+    const QDomNodeList props = response.elementsByTagName(QStringLiteral("prop"));
+    const QDomElement prop = props.at(0).toElement();
+
+    QDomElement element = prop.firstChildElement();
+    while (!element.isNull()) {
+        if (element.tagName() == QLatin1String("last_modified")) {
+            mObject.setLastModified(OXUtils::readString(element.text()));
+        }
+
+        element = element.nextSiblingElement();
+    }
+
+    emitResult();
+}
+
diff --git a/resources/openxchange/oxa/objectmodifyjob.h b/resources/openxchange/oxa/objectmodifyjob.h
new file mode 100644 (file)
index 0000000..f2ea7cc
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_OBJECTMODIFYJOB_H
+#define OXA_OBJECTMODIFYJOB_H
+
+#include <kjob.h>
+
+#include "object.h"
+
+namespace OXA
+{
+
+class ObjectModifyJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    explicit ObjectModifyJob(const Object &object, QObject *parent = Q_NULLPTR);
+
+    void start() Q_DECL_OVERRIDE;
+
+    Object object() const;
+
+private Q_SLOTS:
+    void preloadingJobFinished(KJob *);
+    void davJobFinished(KJob *);
+
+private:
+    Object mObject;
+};
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/objectmovejob.cpp b/resources/openxchange/oxa/objectmovejob.cpp
new file mode 100644 (file)
index 0000000..079f7e1
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "objectmovejob.h"
+
+#include "davmanager.h"
+#include "davutils.h"
+#include "objectutils.h"
+#include "oxutils.h"
+
+#include <kio/davjob.h>
+
+using namespace OXA;
+
+ObjectMoveJob::ObjectMoveJob(const Object &object, const Folder &destinationFolder, QObject *parent)
+    : KJob(parent), mObject(object), mDestinationFolder(destinationFolder)
+{
+}
+
+void ObjectMoveJob::start()
+{
+    QDomDocument document;
+    QDomElement propertyupdate = DAVUtils::addDavElement(document, document, QStringLiteral("propertyupdate"));
+    QDomElement set = DAVUtils::addDavElement(document, propertyupdate, QStringLiteral("set"));
+    QDomElement prop = DAVUtils::addDavElement(document, set, QStringLiteral("prop"));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("object_id"), OXUtils::writeNumber(mObject.objectId()));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("folder_id"), OXUtils::writeNumber(mObject.folderId()));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("last_modified"), OXUtils::writeString(mObject.lastModified()));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("folder"), OXUtils::writeNumber(mDestinationFolder.objectId()));
+
+    const QString path = ObjectUtils::davPath(mObject.module());
+
+    KIO::DavJob *job = DavManager::self()->createPatchJob(path, document);
+    connect(job, &KIO::DavJob::result, this, &ObjectMoveJob::davJobFinished);
+}
+
+Object ObjectMoveJob::object() const
+{
+    return mObject;
+}
+
+void ObjectMoveJob::davJobFinished(KJob *job)
+{
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+        emitResult();
+        return;
+    }
+
+    KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
+
+    const QDomDocument document = davJob->response();
+
+    QString errorText, errorStatus;
+    if (DAVUtils::davErrorOccurred(document, errorText, errorStatus)) {
+        setError(UserDefinedError);
+        setErrorText(errorText);
+        emitResult();
+        return;
+    }
+
+    QDomElement multistatus = document.documentElement();
+    QDomElement response = multistatus.firstChildElement(QStringLiteral("response"));
+    const QDomNodeList props = response.elementsByTagName(QStringLiteral("prop"));
+    const QDomElement prop = props.at(0).toElement();
+
+    QDomElement element = prop.firstChildElement();
+    while (!element.isNull()) {
+        if (element.tagName() == QLatin1String("last_modified")) {
+            mObject.setLastModified(OXUtils::readString(element.text()));
+        }
+
+        element = element.nextSiblingElement();
+    }
+
+    emitResult();
+}
+
diff --git a/resources/openxchange/oxa/objectmovejob.h b/resources/openxchange/oxa/objectmovejob.h
new file mode 100644 (file)
index 0000000..3353817
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_OBJECTMOVEJOB_H
+#define OXA_OBJECTMOVEJOB_H
+
+#include <kjob.h>
+
+#include "folder.h"
+#include "object.h"
+
+namespace OXA
+{
+
+class ObjectMoveJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    ObjectMoveJob(const Object &object, const Folder &destinationFolder, QObject *parent = Q_NULLPTR);
+
+    void start() Q_DECL_OVERRIDE;
+
+    Object object() const;
+
+private Q_SLOTS:
+    void davJobFinished(KJob *);
+
+private:
+    Object mObject;
+    Folder mDestinationFolder;
+};
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/objectrequestjob.cpp b/resources/openxchange/oxa/objectrequestjob.cpp
new file mode 100644 (file)
index 0000000..32b60dc
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "objectrequestjob.h"
+
+#include "davmanager.h"
+#include "davutils.h"
+#include "objectutils.h"
+#include "oxutils.h"
+
+#include <kio/davjob.h>
+
+using namespace OXA;
+
+ObjectRequestJob::ObjectRequestJob(const Object &object, QObject *parent)
+    : KJob(parent), mObject(object)
+{
+}
+
+void ObjectRequestJob::start()
+{
+    QDomDocument document;
+    QDomElement multistatus = DAVUtils::addDavElement(document, document, QStringLiteral("multistatus"));
+    QDomElement prop = DAVUtils::addDavElement(document, multistatus, QStringLiteral("prop"));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("object_id"), OXUtils::writeNumber(mObject.objectId()));
+
+    const QString path = ObjectUtils::davPath(mObject.module());
+
+    KIO::DavJob *job = DavManager::self()->createFindJob(path, document);
+    connect(job, &KIO::DavJob::result, this, &ObjectRequestJob::davJobFinished);
+}
+
+Object ObjectRequestJob::object() const
+{
+    return mObject;
+}
+
+void ObjectRequestJob::davJobFinished(KJob *job)
+{
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+        emitResult();
+        return;
+    }
+
+    KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
+
+    const QDomDocument document = davJob->response();
+
+    QString errorText, errorStatus;
+    if (DAVUtils::davErrorOccurred(document, errorText, errorStatus)) {
+        setError(UserDefinedError);
+        setErrorText(errorText);
+        emitResult();
+        return;
+    }
+
+    QDomElement multistatus = document.documentElement();
+    QDomElement response = multistatus.firstChildElement(QStringLiteral("response"));
+    const QDomNodeList props = response.elementsByTagName(QStringLiteral("prop"));
+    const QDomElement prop = props.at(0).toElement();
+    mObject = ObjectUtils::parseObject(prop, mObject.module());
+
+    emitResult();
+}
+
diff --git a/resources/openxchange/oxa/objectrequestjob.h b/resources/openxchange/oxa/objectrequestjob.h
new file mode 100644 (file)
index 0000000..eae6b05
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_OBJECTREQUESTJOB_H
+#define OXA_OBJECTREQUESTJOB_H
+
+#include <kjob.h>
+
+#include "object.h"
+
+namespace OXA
+{
+
+class ObjectRequestJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    explicit ObjectRequestJob(const Object &object, QObject *parent = Q_NULLPTR);
+
+    void start() Q_DECL_OVERRIDE;
+
+    Object object() const;
+
+private Q_SLOTS:
+    void davJobFinished(KJob *);
+
+private:
+    Object mObject;
+};
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/objectsrequestdeltajob.cpp b/resources/openxchange/oxa/objectsrequestdeltajob.cpp
new file mode 100644 (file)
index 0000000..1435bf9
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "objectsrequestdeltajob.h"
+
+#include "objectsrequestjob.h"
+
+#include <QtCore/QDebug>
+
+using namespace OXA;
+
+ObjectsRequestDeltaJob::ObjectsRequestDeltaJob(const Folder &folder, qulonglong lastSync, QObject *parent)
+    : KJob(parent), mFolder(folder), mLastSync(lastSync), mJobFinishedCount(0)
+{
+}
+
+void ObjectsRequestDeltaJob::start()
+{
+    ObjectsRequestJob *modifiedJob = new ObjectsRequestJob(mFolder, mLastSync, ObjectsRequestJob::Modified, this);
+    connect(modifiedJob, &ObjectsRequestJob::result, this, &ObjectsRequestDeltaJob::fetchModifiedJobFinished);
+    modifiedJob->start();
+
+    ObjectsRequestJob *deletedJob = new ObjectsRequestJob(mFolder, mLastSync, ObjectsRequestJob::Deleted, this);
+    connect(deletedJob, &ObjectsRequestJob::result, this, &ObjectsRequestDeltaJob::fetchDeletedJobFinished);
+    deletedJob->start();
+}
+
+Object::List ObjectsRequestDeltaJob::modifiedObjects() const
+{
+    return mModifiedObjects;
+}
+
+Object::List ObjectsRequestDeltaJob::deletedObjects() const
+{
+    return mDeletedObjects;
+}
+
+void ObjectsRequestDeltaJob::fetchModifiedJobFinished(KJob *job)
+{
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+        emitResult();
+        return;
+    }
+
+    const ObjectsRequestJob *requestJob = qobject_cast<ObjectsRequestJob *>(job);
+
+    mModifiedObjects << requestJob->objects();
+
+    mJobFinishedCount++;
+
+    if (mJobFinishedCount == 2) {
+        emitResult();
+    }
+}
+
+void ObjectsRequestDeltaJob::fetchDeletedJobFinished(KJob *job)
+{
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+        emitResult();
+        return;
+    }
+
+    const ObjectsRequestJob *requestJob = qobject_cast<ObjectsRequestJob *>(job);
+
+    mDeletedObjects << requestJob->objects();
+
+    mJobFinishedCount++;
+
+    if (mJobFinishedCount == 2) {
+        emitResult();
+    }
+}
+
diff --git a/resources/openxchange/oxa/objectsrequestdeltajob.h b/resources/openxchange/oxa/objectsrequestdeltajob.h
new file mode 100644 (file)
index 0000000..c580f99
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_OBJECTSREQUESTDELTAJOB_H
+#define OXA_OBJECTSREQUESTDELTAJOB_H
+
+#include <kjob.h>
+
+#include "object.h"
+
+namespace OXA
+{
+
+/**
+ * @short A job that requests the delta for objects changes from the OX server.
+ *
+ * @author Tobias Koenig <tokoe@kde.org>
+ */
+class ObjectsRequestDeltaJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Creates a new objects request delta job.
+     *
+     * @param folder The folder the objects shall be request from.
+     * @param lastSync The timestamp of the last sync. Only added, modified and deleted objects
+     *                 after this date will be requested. 0 will request all available objects.
+     * @param parent The parent object.
+     */
+    ObjectsRequestDeltaJob(const Folder &folder, qulonglong lastSync, QObject *parent = Q_NULLPTR);
+
+    /**
+     * Starts the job.
+     */
+    void start() Q_DECL_OVERRIDE;
+
+    /**
+     * Returns the list of all added and modified objects.
+     */
+    Object::List modifiedObjects() const;
+
+    /**
+     * Returns the list of all deleted objects.
+     */
+    Object::List deletedObjects() const;
+
+private Q_SLOTS:
+    void fetchModifiedJobFinished(KJob *);
+    void fetchDeletedJobFinished(KJob *);
+
+private:
+    Folder mFolder;
+    qulonglong mLastSync;
+    Object::List mModifiedObjects;
+    Object::List mDeletedObjects;
+    int mJobFinishedCount;
+};
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/objectsrequestjob.cpp b/resources/openxchange/oxa/objectsrequestjob.cpp
new file mode 100644 (file)
index 0000000..8ba346e
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "objectsrequestjob.h"
+
+#include "davmanager.h"
+#include "davutils.h"
+#include "objectutils.h"
+#include "oxutils.h"
+
+#include <kio/davjob.h>
+
+#include <QtCore/QDebug>
+
+using namespace OXA;
+
+ObjectsRequestJob::ObjectsRequestJob(const Folder &folder, qulonglong lastSync, Mode mode, QObject *parent)
+    : KJob(parent), mFolder(folder), mLastSync(lastSync), mMode(mode)
+{
+}
+
+void ObjectsRequestJob::start()
+{
+    QDomDocument document;
+    QDomElement multistatus = DAVUtils::addDavElement(document, document, QStringLiteral("multistatus"));
+    QDomElement prop = DAVUtils::addDavElement(document, multistatus, QStringLiteral("prop"));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("folder_id"), OXUtils::writeNumber(mFolder.objectId()));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("lastsync"), OXUtils::writeNumber(mLastSync));
+    if (mMode == Modified) {
+        DAVUtils::addOxElement(document, prop, QStringLiteral("objectmode"), QStringLiteral("MODIFIED"));
+    } else {
+        DAVUtils::addOxElement(document, prop, QStringLiteral("objectmode"), QStringLiteral("DELETED"));
+    }
+
+    const QString path = ObjectUtils::davPath(mFolder.module());
+
+    KIO::DavJob *job = DavManager::self()->createFindJob(path, document);
+    connect(job, &KIO::DavJob::result, this, &ObjectsRequestJob::davJobFinished);
+}
+
+Object::List ObjectsRequestJob::objects() const
+{
+    return mObjects;
+}
+
+void ObjectsRequestJob::davJobFinished(KJob *job)
+{
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+        emitResult();
+        return;
+    }
+
+    KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
+
+    const QDomDocument document = davJob->response();
+
+    QString errorText, errorStatus;
+    if (DAVUtils::davErrorOccurred(document, errorText, errorStatus)) {
+        setError(UserDefinedError);
+        setErrorText(errorText);
+        emitResult();
+        return;
+    }
+
+    QDomElement multistatus = document.documentElement();
+    QDomElement response = multistatus.firstChildElement(QStringLiteral("response"));
+    while (!response.isNull()) {
+        const QDomNodeList props = response.elementsByTagName(QStringLiteral("prop"));
+        const QDomElement prop = props.at(0).toElement();
+        mObjects.append(ObjectUtils::parseObject(prop, mFolder.module()));
+        response = response.nextSiblingElement();
+    }
+
+    emitResult();
+}
+
diff --git a/resources/openxchange/oxa/objectsrequestjob.h b/resources/openxchange/oxa/objectsrequestjob.h
new file mode 100644 (file)
index 0000000..65a0376
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_OBJECTSREQUESTJOB_H
+#define OXA_OBJECTSREQUESTJOB_H
+
+#include <kjob.h>
+
+#include "folder.h"
+#include "object.h"
+
+namespace OXA
+{
+
+class ObjectsRequestJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    /**
+     * Describes the mode of the request job.
+     */
+    enum Mode {
+        Modified,  ///< Fetches all new and modified objects
+        Deleted    ///< Fetches all deleted objects
+    };
+
+    /**
+     * Creates a new objects request job.
+     *
+     * @param folder The folder the objects shall be request from.
+     * @param lastSync The timestamp of the last sync. Only added, modified or deleted objects
+     *                 after this date will be requested. 0 will request all available objects.
+     * @param mode The mode of objects to request.
+     * @param parent The parent object.
+     */
+    explicit ObjectsRequestJob(const Folder &folder, qulonglong lastSync = 0, Mode mode = Modified, QObject *parent = Q_NULLPTR);
+
+    void start() Q_DECL_OVERRIDE;
+
+    Object::List objects() const;
+
+private Q_SLOTS:
+    void davJobFinished(KJob *);
+
+private:
+    Folder mFolder;
+    qulonglong mLastSync;
+    Mode mMode;
+    Object::List mObjects;
+};
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/objectutils.cpp b/resources/openxchange/oxa/objectutils.cpp
new file mode 100644 (file)
index 0000000..526fc15
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "objectutils.h"
+
+#include "contactutils.h"
+#include "incidenceutils.h"
+#include "davutils.h"
+#include "oxutils.h"
+
+#include <QtCore/QBuffer>
+#include <QtXml/QDomElement>
+
+using namespace OXA;
+
+Object OXA::ObjectUtils::parseObject(const QDomElement &propElement, Folder::Module module)
+{
+    Object object;
+
+    QDomElement element = propElement.firstChildElement();
+    while (!element.isNull()) {
+        if (element.tagName() == QLatin1String("last_modified")) {
+            object.setLastModified(OXUtils::readString(element.text()));
+        } else if (element.tagName() == QLatin1String("object_id")) {
+            object.setObjectId(OXUtils::readNumber(element.text()));
+        } else if (element.tagName() == QLatin1String("folder_id")) {
+            object.setFolderId(OXUtils::readNumber(element.text()));
+        } else if (element.tagName() == QLatin1String("object_status")) {
+            const QString content = OXUtils::readString(element.text());
+            if (content == QLatin1String("CREATE")) {
+                object.setObjectStatus(Object::Created);
+            } else if (content == QLatin1String("DELETE")) {
+                object.setObjectStatus(Object::Deleted);
+            } else {
+                Q_ASSERT(false);
+            }
+        }
+
+        element = element.nextSiblingElement();
+    }
+
+    switch (module) {
+    case Folder::Contacts: ContactUtils::parseContact(propElement, object); break;
+    case Folder::Calendar: IncidenceUtils::parseEvent(propElement, object); break;
+    case Folder::Tasks: IncidenceUtils::parseTask(propElement, object); break;
+    case Folder::Unbound: Q_ASSERT(false); break;
+    }
+
+    return object;
+}
+
+void OXA::ObjectUtils::addObjectElements(QDomDocument &document, QDomElement &propElement, const Object &object, void *preloadedData)
+{
+    if (object.objectId() != -1) {
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("object_id"), OXUtils::writeNumber(object.objectId()));
+    }
+    if (object.folderId() != -1) {
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("folder_id"), OXUtils::writeNumber(object.folderId()));
+    }
+    if (!object.lastModified().isEmpty()) {
+        DAVUtils::addOxElement(document, propElement, QStringLiteral("last_modified"), OXUtils::writeString(object.lastModified()));
+    }
+
+    switch (object.module()) {
+    case Folder::Contacts: ContactUtils::addContactElements(document, propElement, object, preloadedData); break;
+    case Folder::Calendar: IncidenceUtils::addEventElements(document, propElement, object); break;
+    case Folder::Tasks: IncidenceUtils::addTaskElements(document, propElement, object); break;
+    case Folder::Unbound: Q_ASSERT(false); break;
+    }
+}
+
+bool OXA::ObjectUtils::needsPreloading(const Object &object)
+{
+    if (object.module() == Folder::Contacts) {
+        if (object.contactGroup().contactReferenceCount() != 0) { // we have to resolve these entries first
+            return true;
+        }
+    }
+
+    return false;
+}
+
+KJob *OXA::ObjectUtils::preloadJob(const Object &object)
+{
+    if (object.module() == Folder::Contacts) {
+        if (object.contactGroup().contactReferenceCount() != 0) {
+            return ContactUtils::preloadJob(object);
+        }
+    }
+
+    return Q_NULLPTR;
+}
+
+void *OXA::ObjectUtils::preloadData(const Object &object, KJob *job)
+{
+    if (object.module() == Folder::Contacts) {
+        if (object.contactGroup().contactReferenceCount() != 0) {
+            return ContactUtils::preloadData(object, job);
+        }
+    }
+
+    return Q_NULLPTR;
+}
+
+QString OXA::ObjectUtils::davPath(Folder::Module module)
+{
+    switch (module) {
+    case Folder::Contacts: return QStringLiteral("/servlet/webdav.contacts");
+    case Folder::Calendar: return QStringLiteral("/servlet/webdav.calendar");
+    case Folder::Tasks: return QStringLiteral("/servlet/webdav.tasks");
+    case Folder::Unbound: Q_ASSERT(false); return QString();
+    }
+
+    return QString();
+}
diff --git a/resources/openxchange/oxa/objectutils.h b/resources/openxchange/oxa/objectutils.h
new file mode 100644 (file)
index 0000000..913ea85
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_OBJECTUTILS_H
+#define OXA_OBJECTUTILS_H
+
+#include "folder.h"
+#include "object.h"
+
+class KJob;
+
+class QDomDocument;
+class QDomElement;
+
+namespace OXA
+{
+
+namespace ObjectUtils
+{
+Object parseObject(const QDomElement &propElement, Folder::Module module);
+void addObjectElements(QDomDocument &document, QDomElement &propElement, const Object &object, void *preloadedData = Q_NULLPTR);
+
+/**
+ * Returns the dav path that is used for the given @p module.
+ */
+QString davPath(Folder::Module module);
+
+/**
+ * On some actions (e.g. creating or modifiying items) we have to preload
+ * data asynchronously. The following methods allow to do that in a generic way.
+ */
+
+/**
+ * Checks whether the @p object needs preloading of data.
+ */
+bool needsPreloading(const Object &object);
+
+/**
+ * Creates a preloading job for the @p object.
+ */
+KJob *preloadJob(const Object &object);
+
+/**
+ * Converts the data loaded by the preloading @p job into pointer
+ * that will be passed to addObjectElements later on.
+ */
+void *preloadData(const Object &object, KJob *job);
+}
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/oxerrors.cpp b/resources/openxchange/oxa/oxerrors.cpp
new file mode 100644 (file)
index 0000000..b134145
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2012 Marco Nelles <marco.nelles@credativ.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "oxerrors.h"
+
+using namespace OXA;
+
+OXErrors::EditErrorID OXErrors::getEditErrorID(const QString &errorText)
+{
+    int b1Pos = errorText.indexOf(QLatin1Char('['));
+    int b2Pos = errorText.indexOf(QLatin1Char(']'));
+    QString errorID = errorText.mid(b1Pos + 1, b2Pos - b1Pos - 1);
+
+    bool ok;
+    int eid = errorID.toInt(&ok);
+    if (!ok) {
+        return OXErrors::EditErrorUndefined;
+    }
+
+    switch (eid) {
+    case 1000 : return OXErrors::ConcurrentModification;
+    case 1001 : return OXErrors::ObjectNotFound;
+    case 1002 : return OXErrors::NoPermissionForThisAction;
+    case 1003 : return OXErrors::ConflictsDetected;
+    case 1004 : return OXErrors::MissingMandatoryFields;
+    case 1006 : return OXErrors::AppointmentConflicts;
+    case 1500 : return OXErrors::InternalServerError;
+    default :;
+    }
+
+    return OXErrors::EditErrorUndefined;
+}
diff --git a/resources/openxchange/oxa/oxerrors.h b/resources/openxchange/oxa/oxerrors.h
new file mode 100644 (file)
index 0000000..b91147a
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2012 Marco Nelles <marco.nelles@credativ.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_OXERRORS_H
+#define OXA_OXERRORS_H
+
+#include <QtCore/QString>
+
+namespace OXA
+{
+
+/**
+ * Namespace that contains methods for handling OX errors.
+ *
+ * @author Marco Nelles <marco.nelles@credativ.com>
+ */
+namespace OXErrors
+{
+
+enum EditErrorID {
+    EditErrorUndefined = 0,
+    ConcurrentModification,
+    ObjectNotFound,
+    NoPermissionForThisAction,
+    ConflictsDetected,
+    MissingMandatoryFields,
+    AppointmentConflicts,
+    InternalServerError
+};
+
+/**
+ * Parse error id from edit error text string @p errorText
+ */
+EditErrorID getEditErrorID(const QString &errorText);
+
+}
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/oxutils.cpp b/resources/openxchange/oxa/oxutils.cpp
new file mode 100644 (file)
index 0000000..6fc6aae
--- /dev/null
@@ -0,0 +1,137 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "oxutils.h"
+
+#include <qdatetime.h>
+
+using namespace OXA;
+
+QString OXUtils::writeBoolean(bool value)
+{
+    return (value ? QStringLiteral("true") : QStringLiteral("false"));
+}
+
+QString OXUtils::writeNumber(qlonglong value)
+{
+    return QString::number(value);
+}
+
+QString OXUtils::writeString(const QString &value)
+{
+    QStringList lines = value.split(QLatin1Char('\n'));
+
+    for (int i = 0; i < lines.count(); ++i) {
+        lines[i].replace(QLatin1Char('\\'), QStringLiteral("\\\\"));
+        lines[i].replace(QLatin1Char('"'), QStringLiteral("\\\""));
+    }
+
+    return lines.join(QStringLiteral("\n"));
+}
+
+QString OXUtils::writeName(const QString &value)
+{
+    //TODO: assert on invalid names
+    return value;
+}
+
+QString OXUtils::writeDateTime(const QDateTime &value)
+{
+    QString result;
+
+    //workaround, as QDateTime does not support negative time_t values
+    QDateTime Time_t_S(QDate(1970, 1, 1), QTime(0, 0, 0), Qt::UTC);
+
+    if (value < Time_t_S) {
+
+        result = QString::number(Time_t_S.secsTo(value));
+
+    } else {
+
+        result = QString::number(value.toUTC().toTime_t());
+
+    }
+
+    return QString(result + QLatin1String("000"));
+
+}
+
+QString OXUtils::writeDate(const QDate &value)
+{
+    return writeDateTime(QDateTime(value, QTime(0, 0, 0), Qt::UTC));
+}
+
+bool OXUtils::readBoolean(const QString &text)
+{
+    if (text == QLatin1String("true")) {
+        return true;
+    } else if (text == QLatin1String("false")) {
+        return false;
+    } else {
+        Q_ASSERT(false);
+        return false;
+    }
+}
+
+qlonglong OXUtils::readNumber(const QString &text)
+{
+    return text.toLongLong();
+}
+
+QString OXUtils::readString(const QString &text)
+{
+    QString value(text);
+    value.replace(QStringLiteral("\\\""), QStringLiteral("\""));
+    value.replace(QStringLiteral("\\\\"), QStringLiteral("\\"));
+
+    return value;
+}
+
+QString OXUtils::readName(const QString &text)
+{
+    return text;
+}
+
+QDateTime OXUtils::readDateTime(const QString &text)
+{
+    // remove the trailing '000', they exceed the integer dimension
+    const int ticks = text.mid(0, text.length() - 3).toLongLong();
+
+    //workaround, as QDateTime does not support negative time_t values
+    QDateTime value;
+    if (ticks < 0) {
+
+        value.setTime_t(0);
+        value = value.addSecs(ticks);
+
+    } else {
+
+        value.setTime_t(ticks);
+
+    }
+
+    return value;
+}
+
+QDate OXUtils::readDate(const QString &text)
+{
+    return readDateTime(text).date();
+}
diff --git a/resources/openxchange/oxa/oxutils.h b/resources/openxchange/oxa/oxutils.h
new file mode 100644 (file)
index 0000000..b96e992
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_OXUTILS_H
+#define OXA_OXUTILS_H
+
+#include <QtCore/QString>
+#include <QtCore/QStringList>
+#include <QtCore/QDateTime>
+
+namespace OXA
+{
+
+namespace OXUtils
+{
+QString writeBoolean(bool value);
+QString writeNumber(qlonglong value);
+QString writeString(const QString &value);
+QString writeName(const QString &value);
+QString writeDateTime(const QDateTime &value);
+QString writeDate(const QDate &value);
+
+bool readBoolean(const QString &text);
+qlonglong readNumber(const QString &text);
+QString readString(const QString &text);
+QString readName(const QString &text);
+QDateTime readDateTime(const QString &text);
+QDate readDate(const QString &text);
+}
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/updateusersjob.cpp b/resources/openxchange/oxa/updateusersjob.cpp
new file mode 100644 (file)
index 0000000..070b7a9
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "updateusersjob.h"
+
+#include "useridrequestjob.h"
+#include "users.h"
+#include "usersrequestjob.h"
+
+using namespace OXA;
+
+UpdateUsersJob::UpdateUsersJob(QObject *parent)
+    : KJob(parent), mUserIdRequestFinished(false), mUsersRequestFinished(false), mUserId(-1)
+{
+}
+
+void UpdateUsersJob::start()
+{
+    UserIdRequestJob *userIdJob = new UserIdRequestJob(this);
+    connect(userIdJob, &UserIdRequestJob::result, this, &UpdateUsersJob::userIdRequestJobFinished);
+
+    UsersRequestJob *usersJob = new UsersRequestJob(this);
+    connect(usersJob, &UsersRequestJob::result, this, &UpdateUsersJob::usersRequestJobFinished);
+
+    userIdJob->start();
+    usersJob->start();
+}
+
+void UpdateUsersJob::userIdRequestJobFinished(KJob *job)
+{
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+    } else {
+        mUserIdRequestFinished = true;
+
+        UserIdRequestJob *requestJob = qobject_cast<UserIdRequestJob *>(job);
+        mUserId = requestJob->userId();
+
+        finish();
+    }
+}
+
+void UpdateUsersJob::usersRequestJobFinished(KJob *job)
+{
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+    } else {
+        mUsersRequestFinished = true;
+
+        UsersRequestJob *requestJob = qobject_cast<UsersRequestJob *>(job);
+        mUsers = requestJob->users();
+
+        finish();
+    }
+}
+
+void UpdateUsersJob::finish()
+{
+    // check if both sub-jobs have finished
+    if (!(mUserIdRequestFinished && mUsersRequestFinished)) {
+        return;
+    }
+
+    if (error()) {
+        emitResult();
+        return;
+    }
+
+    Users::self()->setCurrentUserId(mUserId);
+    Users::self()->setUsers(mUsers);
+
+    emitResult();
+}
+
diff --git a/resources/openxchange/oxa/updateusersjob.h b/resources/openxchange/oxa/updateusersjob.h
new file mode 100644 (file)
index 0000000..ed30ac7
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_UPDATEUSERSJOB_H
+#define OXA_UPDATEUSERSJOB_H
+
+#include <kjob.h>
+
+#include "user.h"
+
+namespace OXA
+{
+
+class UpdateUsersJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    explicit UpdateUsersJob(QObject *parent = Q_NULLPTR);
+
+    void start() Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void userIdRequestJobFinished(KJob *);
+    void usersRequestJobFinished(KJob *);
+
+private:
+    void finish();
+
+    bool mUserIdRequestFinished;
+    bool mUsersRequestFinished;
+    User::List mUsers;
+    qlonglong mUserId;
+};
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/user.cpp b/resources/openxchange/oxa/user.cpp
new file mode 100644 (file)
index 0000000..e134770
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "user.h"
+
+using namespace OXA;
+
+User::User()
+    : mUid(-1)
+{
+}
+
+bool User::isValid() const
+{
+    return (mUid != -1);
+}
+
+void User::setUid(qlonglong uid)
+{
+    mUid = uid;
+}
+
+qlonglong User::uid() const
+{
+    return mUid;
+}
+
+void User::setEmail(const QString &email)
+{
+    mEmail = email;
+}
+
+QString User::email() const
+{
+    return mEmail;
+}
+
+void User::setName(const QString &name)
+{
+    mName = name;
+}
+
+QString User::name() const
+{
+    return mName;
+}
diff --git a/resources/openxchange/oxa/user.h b/resources/openxchange/oxa/user.h
new file mode 100644 (file)
index 0000000..e1aa7ab
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_USER_H
+#define OXA_USER_H
+
+#include <QtCore/QVector>
+#include <QtCore/QString>
+
+namespace OXA
+{
+
+class User
+{
+public:
+    typedef QVector<User> List;
+
+    User();
+
+    bool isValid() const;
+
+    void setUid(qlonglong uid);
+    qlonglong uid() const;
+
+    void setEmail(const QString &email);
+    QString email() const;
+
+    void setName(const QString &name);
+    QString name() const;
+
+private:
+    qlonglong mUid;
+    QString mEmail;
+    QString mName;
+};
+
+}
+
+Q_DECLARE_TYPEINFO(OXA::User, Q_MOVABLE_TYPE);
+#endif
diff --git a/resources/openxchange/oxa/useridrequestjob.cpp b/resources/openxchange/oxa/useridrequestjob.cpp
new file mode 100644 (file)
index 0000000..d116fe2
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "useridrequestjob.h"
+
+#include "foldersrequestjob.h"
+#include "davmanager.h"
+
+#include <QtCore/QDebug>
+
+using namespace OXA;
+
+UserIdRequestJob::UserIdRequestJob(QObject *parent)
+    : KJob(parent), mUserId(-1)
+{
+}
+
+void UserIdRequestJob::start()
+{
+    FoldersRequestJob *job = new FoldersRequestJob(0, FoldersRequestJob::Modified, this);
+    connect(job, &FoldersRequestJob::result, this, &UserIdRequestJob::davJobFinished);
+
+    job->start();
+}
+
+qlonglong UserIdRequestJob::userId() const
+{
+    return mUserId;
+}
+
+void UserIdRequestJob::davJobFinished(KJob *job)
+{
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+        emitResult();
+        return;
+    }
+
+    FoldersRequestJob *requestJob = qobject_cast<FoldersRequestJob *>(job);
+    Q_ASSERT(requestJob);
+
+    const Folder::List folders = requestJob->folders();
+    foreach (const Folder &folder, folders) {
+        if (folder.folderId() == 1) {
+            // Found folder with 'Private Folders' as parent, so the owner must
+            // be the user that is currently logged in.
+            mUserId = folder.owner();
+            break;
+        }
+    }
+
+    if (mUserId == -1) {
+        setError(UserDefinedError);
+        setErrorText(QStringLiteral("No private folder found"));
+    }
+
+    emitResult();
+}
+
diff --git a/resources/openxchange/oxa/useridrequestjob.h b/resources/openxchange/oxa/useridrequestjob.h
new file mode 100644 (file)
index 0000000..7fb7a82
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_USERIDREQUESTJOB_H
+#define OXA_USERIDREQUESTJOB_H
+
+#include <kjob.h>
+
+namespace OXA
+{
+
+class UserIdRequestJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    explicit UserIdRequestJob(QObject *parent = Q_NULLPTR);
+
+    void start() Q_DECL_OVERRIDE;
+
+    qlonglong userId() const;
+
+private Q_SLOTS:
+    void davJobFinished(KJob *);
+
+private:
+    qlonglong mUserId;
+};
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/users.cpp b/resources/openxchange/oxa/users.cpp
new file mode 100644 (file)
index 0000000..5a86648
--- /dev/null
@@ -0,0 +1,159 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "users.h"
+
+#include <QtCore/QFile>
+#include <QStandardPaths>
+#include <QDataStream>
+
+using namespace OXA;
+
+Users *Users::mSelf = Q_NULLPTR;
+
+Users::Users()
+    : mCurrentUserId(-1)
+{
+}
+
+Users::~Users()
+{
+}
+
+Users *Users::self()
+{
+    if (!mSelf) {
+        mSelf = new Users();
+    }
+
+    return mSelf;
+}
+
+void Users::init(const QString &identifier)
+{
+    mIdentifier = identifier;
+
+    loadFromCache();
+}
+
+qlonglong Users::currentUserId() const
+{
+    return mCurrentUserId;
+}
+
+User Users::lookupUid(qlonglong uid) const
+{
+    return mUsers.value(uid);
+}
+
+User Users::lookupEmail(const QString &email) const
+{
+    QMapIterator<qlonglong, User> it(mUsers);
+    while (it.hasNext()) {
+        it.next();
+
+        if (it.value().email() == email) {
+            return it.value();
+        }
+    }
+
+    return User();
+}
+
+QString Users::cacheFilePath() const
+{
+    return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + QLatin1String("openxchangeresource_") + mIdentifier;
+}
+
+void Users::setCurrentUserId(qlonglong id)
+{
+    mCurrentUserId = id;
+
+    saveToCache();
+}
+
+void Users::setUsers(const User::List &users)
+{
+    mUsers.clear();
+
+    foreach (const User &user, users) {
+        mUsers.insert(user.uid(), user);
+    }
+
+    saveToCache();
+}
+
+void Users::loadFromCache()
+{
+    QFile cacheFile(cacheFilePath());
+    if (!cacheFile.open(QIODevice::ReadOnly)) {
+        return;
+    }
+
+    QDataStream stream(&cacheFile);
+    stream.setVersion(QDataStream::Qt_4_6);
+
+    mUsers.clear();
+
+    stream >> mCurrentUserId;
+
+    qulonglong count;
+    stream >> count;
+
+    qlonglong uid;
+    QString name;
+    QString email;
+    for (qulonglong i = 0; i < count; ++i) {
+        stream >> uid >> name >> email;
+
+        User user;
+        user.setUid(uid);
+        user.setName(name);
+        user.setEmail(email);
+        mUsers.insert(user.uid(), user);
+    }
+}
+
+void Users::saveToCache()
+{
+    QFile cacheFile(cacheFilePath());
+    if (!cacheFile.open(QIODevice::WriteOnly)) {
+        return;
+    }
+
+    QDataStream stream(&cacheFile);
+    stream.setVersion(QDataStream::Qt_4_6);
+
+    // write current user id
+    stream << mCurrentUserId;
+
+    // write number of users
+    stream << (qulonglong)mUsers.count();
+
+    // write uid, name and email address for each user
+    QMapIterator<qlonglong, User> it(mUsers);
+    while (it.hasNext()) {
+        it.next();
+
+        stream << it.value().uid() << it.value().name() << it.value().email();
+    }
+}
+
diff --git a/resources/openxchange/oxa/users.h b/resources/openxchange/oxa/users.h
new file mode 100644 (file)
index 0000000..97c394c
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_USERS_H
+#define OXA_USERS_H
+
+#include "user.h"
+
+#include <QtCore/QMap>
+#include <QtCore/QObject>
+
+namespace OXA
+{
+
+class Users : public QObject
+{
+    Q_OBJECT
+
+public:
+    ~Users();
+
+    static Users *self();
+
+    void init(const QString &identifier);
+
+    qlonglong currentUserId() const;
+
+    User lookupUid(qlonglong uid) const;
+    User lookupEmail(const QString &email) const;
+
+    QString cacheFilePath() const;
+
+private:
+    friend class UpdateUsersJob;
+
+    Users();
+    void setCurrentUserId(qlonglong);
+    void setUsers(const User::List &);
+
+    void loadFromCache();
+    void saveToCache();
+
+    qlonglong mCurrentUserId;
+    QMap<qlonglong, User> mUsers;
+    QString mIdentifier;
+
+    static Users *mSelf;
+};
+
+}
+
+#endif
diff --git a/resources/openxchange/oxa/usersrequestjob.cpp b/resources/openxchange/oxa/usersrequestjob.cpp
new file mode 100644 (file)
index 0000000..51e8e91
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "usersrequestjob.h"
+
+#include "davmanager.h"
+#include "davutils.h"
+#include "oxutils.h"
+
+#include <kio/davjob.h>
+
+using namespace OXA;
+
+UsersRequestJob::UsersRequestJob(QObject *parent)
+    : KJob(parent)
+{
+}
+
+void UsersRequestJob::start()
+{
+    QDomDocument document;
+    QDomElement multistatus = DAVUtils::addDavElement(document, document, QStringLiteral("multistatus"));
+    QDomElement prop = DAVUtils::addDavElement(document, multistatus, QStringLiteral("prop"));
+    DAVUtils::addOxElement(document, prop, QStringLiteral("user"), QStringLiteral("*"));
+
+    const QString path = QStringLiteral("/servlet/webdav.groupuser");
+
+    KIO::DavJob *job = DavManager::self()->createFindJob(path, document);
+    connect(job, &KIO::DavJob::result, this, &UsersRequestJob::davJobFinished);
+
+    job->start();
+}
+
+User::List UsersRequestJob::users() const
+{
+    return mUsers;
+}
+
+void UsersRequestJob::davJobFinished(KJob *job)
+{
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+        emitResult();
+        return;
+    }
+
+    KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
+
+    const QDomDocument document = davJob->response();
+
+    QDomElement multistatus = document.documentElement();
+    QDomElement response = multistatus.firstChildElement(QStringLiteral("response"));
+    QDomElement propstat = response.firstChildElement(QStringLiteral("propstat"));
+    QDomElement prop = propstat.firstChildElement(QStringLiteral("prop"));
+    QDomElement users = prop.firstChildElement(QStringLiteral("users"));
+
+    QDomElement userElement = users.firstChildElement(QStringLiteral("user"));
+    while (!userElement.isNull()) {
+        User user;
+
+        QDomElement element = userElement.firstChildElement();
+        while (!element.isNull()) {
+            if (element.tagName() == QLatin1String("uid")) {
+                user.setUid(OXUtils::readNumber(element.text()));
+            } else if (element.tagName() == QLatin1String("email1")) {
+                user.setEmail(OXUtils::readString(element.text()));
+            } else if (element.tagName() == QLatin1String("displayname")) {
+                user.setName(OXUtils::readString(element.text()));
+            }
+
+            element = element.nextSiblingElement();
+        }
+
+        mUsers.append(user);
+
+        userElement = userElement.nextSiblingElement(QStringLiteral("user"));
+    }
+
+    emitResult();
+}
+
diff --git a/resources/openxchange/oxa/usersrequestjob.h b/resources/openxchange/oxa/usersrequestjob.h
new file mode 100644 (file)
index 0000000..61de3fe
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+    This file is part of oxaccess.
+
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef OXA_USERSREQUESTJOB_H
+#define OXA_USERSREQUESTJOB_H
+
+#include <kjob.h>
+
+#include "user.h"
+
+namespace OXA
+{
+
+class UsersRequestJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    explicit UsersRequestJob(QObject *parent = Q_NULLPTR);
+
+    void start() Q_DECL_OVERRIDE;
+
+    User::List users() const;
+
+private Q_SLOTS:
+    void davJobFinished(KJob *);
+
+private:
+    User::List mUsers;
+};
+
+}
+
+#endif
diff --git a/resources/openxchange/settings.kcfgc b/resources/openxchange/settings.kcfgc
new file mode 100644 (file)
index 0000000..17793a8
--- /dev/null
@@ -0,0 +1,7 @@
+File=openxchangeresource.kcfg
+ClassName=Settings
+Mutators=true
+ItemAccessors=true
+SetUserTexts=true
+Singleton=true
+GlobalEnums=true
diff --git a/resources/pop3/CMakeLists.txt b/resources/pop3/CMakeLists.txt
new file mode 100644 (file)
index 0000000..8723a36
--- /dev/null
@@ -0,0 +1,59 @@
+project(pop3)
+
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_pop3_resource\")
+
+
+
+########### next target ###############
+
+set( pop3resource_SRCS
+  pop3resource.cpp
+  accountdialog.cpp
+  jobs.cpp
+  settings.cpp
+)
+
+ecm_qt_declare_logging_category(pop3resource_SRCS HEADER pop3resource_debug.h IDENTIFIER POP3RESOURCE_LOG CATEGORY_NAME log_pop3resource)
+
+install( FILES pop3resource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents" )
+
+ki18n_wrap_ui( pop3resource_SRCS popsettings.ui)
+kconfig_add_kcfg_files(pop3resource_SRCS settingsbase.kcfgc)
+kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/settings.kcfg org.kde.Akonadi.POP3.Settings)
+qt5_add_dbus_adaptor(pop3resource_SRCS
+  ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.POP3.Settings.xml settings.h Settings
+)
+
+#add_executable(akonadi_pop3_resource RUN_UNINSTALLED ${pop3resource_SRCS})
+add_executable(akonadi_pop3_resource ${pop3resource_SRCS})
+
+if( APPLE )
+  set_target_properties(akonadi_pop3_resource PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../Info.plist.template)
+  set_target_properties(akonadi_pop3_resource PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.POP3")
+  set_target_properties(akonadi_pop3_resource PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi POP3 Resource")
+endif ()
+
+
+target_link_libraries( akonadi_pop3_resource
+  KF5::AkonadiCore
+  KF5::AkonadiMime
+  KF5::KIOCore
+  KF5::Mime
+  KF5::MailTransport
+  KF5::AkonadiAgentBase
+  KF5::AkonadiWidgets
+  KF5::I18n
+  KF5::TextWidgets
+  KF5::Completion
+  KF5::WidgetsAddons
+  KF5::WindowSystem
+  Qt5::DBus
+)
+
+if(BUILD_TESTING)
+    add_subdirectory( autotests )
+endif()
+
+add_subdirectory( wizard )
+
+install(TARGETS akonadi_pop3_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
diff --git a/resources/pop3/Messages.sh b/resources/pop3/Messages.sh
new file mode 100644 (file)
index 0000000..c41e8c8
--- /dev/null
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+$EXTRACTRC *.ui *.kcfg >> rc.cpp
+$XGETTEXT *.cpp -o $podir/akonadi_pop3_resource.pot
diff --git a/resources/pop3/TODO b/resources/pop3/TODO
new file mode 100644 (file)
index 0000000..5f6a1ca
--- /dev/null
@@ -0,0 +1,49 @@
+====
+TODO
+====
+
+BUGS
+----
+
+- additional \n in item, see the test
+- config dialog: enter key selects collection, and not the Ok button
+- not storing password does not work
+
+FEATURES
+--------
+
+- filter on server
+- delete on server
+- fix "include in manual mail check", should be something in the akonadi libs
+- migration from old KMail accounts
+  -> now DeleteLater group
+  -> store-passwd -> store_passwd (also: use_ssl, use_tls and all the others
+- progressbar integration for client apps, especially the old SSL icon stuff
+- get rid of pop3 ioslave
+- help (there is a a help button in the config dialog)
+  -> maybe copy over most of KMail's POP3 help
+
+CLEANUP
+-------
+
+- api documentation (also for fakeserver)
+- clean up cmakefiles, remove code duplication with kdepimlibs
+- kconfigxt: labels, tooltips, whatsthis etc
+
+UNIT TESTS
+----------
+  - test ssl/tls
+  - test with disabled cache
+  - all leave on server rules
+      -> leave the last x messages
+      -> leave the last x MB
+      -> every leave rule mixed
+  - test cancel mail + seenuidlist
+  - weird bugs in svn log of old kmail
+  - add fails -> second check will still work
+  - correct error messages passed from pop3 server?
+  - byte stuffing
+  - pipelining
+  - abort requested during mail check
+    -> no duplicate messages on next mail check, already added mails added to the seen
+       UID list
diff --git a/resources/pop3/accountdialog.cpp b/resources/pop3/accountdialog.cpp
new file mode 100644 (file)
index 0000000..88ca298
--- /dev/null
@@ -0,0 +1,664 @@
+/*
+ *   Copyright (C) 2000 Espen Sand, espen@kde.org
+ *   Copyright 2009 Thomas McGuire <mcguire@kde.org>
+ *   Copyright (c) 2009-2010 Klaralvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
+ *   Copyright (C) 2010 Casey Link <unnamedrambler@gmail.com>
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License along
+ *   with this program; if not, write to the Free Software Foundation, Inc.,
+ *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ */
+
+// Local includes
+#include "accountdialog.h"
+#include "pop3resource.h"
+#include "settings.h"
+#include "settingsadaptor.h"
+
+// KDEPIMLIBS includes
+#include <Collection>
+#include <CollectionFetchJob>
+#include <Akonadi/KMime/SpecialMailCollections>
+#include <Akonadi/KMime/SpecialMailCollectionsRequestJob>
+#include <resourcesettings.h>
+#include <MailTransport/ServerTest>
+
+// KDELIBS includes
+#include <KEMailSettings>
+#include <KMessageBox>
+#include <KUser>
+#include <KWindowSystem>
+#include <kwallet.h>
+#include "pop3resource_debug.h"
+#include <QDialogButtonBox>
+#include <QPushButton>
+#include <QVBoxLayout>
+
+using namespace MailTransport;
+using namespace Akonadi;
+using namespace KWallet;
+
+namespace
+{
+
+class BusyCursorHelper : public QObject
+{
+public:
+    inline BusyCursorHelper(QObject *parent)
+        : QObject(parent)
+    {
+#ifndef QT_NO_CURSOR
+        qApp->setOverrideCursor(Qt::BusyCursor);
+#endif
+    }
+
+    inline ~BusyCursorHelper()
+    {
+#ifndef QT_NO_CURSOR
+        qApp->restoreOverrideCursor();
+#endif
+    }
+};
+
+}
+
+AccountDialog::AccountDialog(POP3Resource *parentResource, WId parentWindow)
+    : QDialog(),
+      mParentResource(parentResource),
+      mServerTest(Q_NULLPTR),
+      mValidator(this),
+      mWallet(Q_NULLPTR)
+{
+    KWindowSystem::setMainWindow(this, parentWindow);
+    setWindowIcon(QIcon::fromTheme(QStringLiteral("network-server")));
+    setWindowTitle(i18n("POP3 Account Settings"));
+    mValidator.setRegExp(QRegExp(QLatin1String("[A-Za-z0-9-_:.]*")));
+
+    setupWidgets();
+    loadSettings();
+}
+
+AccountDialog::~AccountDialog()
+{
+    delete mWallet;
+    mWallet = Q_NULLPTR;
+    delete mServerTest;
+    mServerTest = Q_NULLPTR;
+}
+
+void AccountDialog::setupWidgets()
+{
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    setLayout(mainLayout);
+    mOkButton = buttonBox->button(QDialogButtonBox::Ok);
+    mOkButton->setDefault(true);
+    mOkButton->setShortcut(Qt::CTRL | Qt::Key_Return);
+    connect(buttonBox, &QDialogButtonBox::accepted, this, &AccountDialog::slotAccepted);
+    connect(buttonBox, &QDialogButtonBox::rejected, this, &AccountDialog::reject);
+
+    QWidget *page = new QWidget(this);
+    mainLayout->addWidget(page);
+    mainLayout->addWidget(buttonBox);
+
+    setupUi(page);
+
+    // only letters, digits, '-', '.', ':' (IPv6) and '_' (for Windows
+    // compatibility) are allowed
+    hostEdit->setValidator(&mValidator);
+    intervalSpin->setSuffix(ki18np(" minute", " minutes"));
+
+    intervalSpin->setRange(ResourceSettings::self()->minimumCheckInterval(), 10000);
+    intervalSpin->setSingleStep(1);
+
+    connect(leaveOnServerCheck, &QCheckBox::clicked, this, &AccountDialog::slotLeaveOnServerClicked);
+    connect(leaveOnServerDaysCheck, &QCheckBox::toggled, this, &AccountDialog::slotEnableLeaveOnServerDays);
+    connect(leaveOnServerDaysSpin, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &AccountDialog::slotLeaveOnServerDaysChanged);
+    connect(leaveOnServerCountCheck, &QCheckBox::toggled, this, &AccountDialog::slotEnableLeaveOnServerCount);
+    connect(leaveOnServerCountSpin, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &AccountDialog::slotLeaveOnServerCountChanged);
+    connect(leaveOnServerSizeCheck, &QCheckBox::toggled, this, &AccountDialog::slotEnableLeaveOnServerSize);
+
+    connect(filterOnServerSizeSpin, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &AccountDialog::slotFilterOnServerSizeChanged);
+    connect(filterOnServerCheck, &QCheckBox::toggled, filterOnServerSizeSpin, &QSpinBox::setEnabled);
+    connect(filterOnServerCheck, &QCheckBox::clicked, this, &AccountDialog::slotFilterOnServerClicked);
+
+    connect(checkCapabilities, &QPushButton::clicked, this, &AccountDialog::slotCheckPopCapabilities);
+    encryptionButtonGroup = new QButtonGroup();
+    encryptionButtonGroup->addButton(encryptionNone,
+                                     Transport::EnumEncryption::None);
+    encryptionButtonGroup->addButton(encryptionSSL,
+                                     Transport::EnumEncryption::SSL);
+    encryptionButtonGroup->addButton(encryptionTLS,
+                                     Transport::EnumEncryption::TLS);
+
+    connect(encryptionButtonGroup, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), this, &AccountDialog::slotPopEncryptionChanged);
+    connect(intervalCheck, &QCheckBox::toggled, this, &AccountDialog::slotEnablePopInterval);
+
+    populateDefaultAuthenticationOptions();
+
+    folderRequester->setMimeTypeFilter(
+        QStringList() << QStringLiteral("message/rfc822"));
+    folderRequester->setAccessRightsFilter(Akonadi::Collection::CanCreateItem);
+    folderRequester->changeCollectionDialogOptions(Akonadi::CollectionDialog::AllowToCreateNewChildCollection);
+
+    connect(usePipeliningCheck, &QCheckBox::clicked, this, &AccountDialog::slotPipeliningClicked);
+
+    // FIXME: Hide widgets which are not supported yet
+    filterOnServerCheck->hide();
+    filterOnServerSizeSpin->hide();
+}
+
+void AccountDialog::loadSettings()
+{
+    if (mParentResource->name() == mParentResource->identifier()) {
+        mParentResource->setName(i18n("POP3 Account"));
+    }
+
+    nameEdit->setText(mParentResource->name());
+    nameEdit->setFocus();
+    loginEdit->setText(!Settings::self()->login().isEmpty() ? Settings::self()->login() :
+                       KUser().loginName());
+
+    hostEdit->setText(
+        !Settings::self()->host().isEmpty() ? Settings::self()->host() :
+        KEMailSettings().getSetting(KEMailSettings::InServer));
+    hostEdit->setText(Settings::self()->host());
+    portEdit->setValue(Settings::self()->port());
+    precommand->setText(Settings::self()->precommand());
+    usePipeliningCheck->setChecked(Settings::self()->pipelining());
+    leaveOnServerCheck->setChecked(Settings::self()->leaveOnServer());
+    leaveOnServerDaysCheck->setEnabled(Settings::self()->leaveOnServer());
+    leaveOnServerDaysCheck->setChecked(Settings::self()->leaveOnServerDays() >= 1);
+    leaveOnServerDaysSpin->setValue(Settings::self()->leaveOnServerDays() >= 1 ?
+                                    Settings::self()->leaveOnServerDays() : 7);
+    leaveOnServerCountCheck->setEnabled(Settings::self()->leaveOnServer());
+    leaveOnServerCountCheck->setChecked(Settings::self()->leaveOnServerCount() >= 1);
+    leaveOnServerCountSpin->setValue(Settings::self()->leaveOnServerCount() >= 1 ?
+                                     Settings::self()->leaveOnServerCount() : 100);
+    leaveOnServerSizeCheck->setEnabled(Settings::self()->leaveOnServer());
+    leaveOnServerSizeCheck->setChecked(Settings::self()->leaveOnServerSize() >= 1);
+    leaveOnServerSizeSpin->setValue(Settings::self()->leaveOnServerSize() >= 1 ?
+                                    Settings::self()->leaveOnServerSize() : 10);
+    filterOnServerCheck->setChecked(Settings::self()->filterOnServer());
+    filterOnServerSizeSpin->setValue(Settings::self()->filterCheckSize());
+    intervalCheck->setChecked(Settings::self()->intervalCheckEnabled());
+    intervalSpin->setValue(Settings::self()->intervalCheckInterval());
+    intervalSpin->setEnabled(Settings::self()->intervalCheckEnabled());
+
+    const int authenticationMethod = Settings::self()->authenticationMethod();
+    authCombo->setCurrentIndex(authCombo->findData(authenticationMethod));
+    encryptionNone->setChecked(!Settings::self()->useSSL() && !Settings::self()->useTLS());
+    encryptionSSL->setChecked(Settings::self()->useSSL());
+    encryptionTLS->setChecked(Settings::self()->useTLS());
+
+    slotEnableLeaveOnServerDays(leaveOnServerDaysCheck->isEnabled() ?
+                                Settings::self()->leaveOnServerDays() >= 1 : false);
+    slotEnableLeaveOnServerCount(leaveOnServerCountCheck->isEnabled() ?
+                                 Settings::self()->leaveOnServerCount() >= 1 : false);
+    slotEnableLeaveOnServerSize(leaveOnServerSizeCheck->isEnabled() ?
+                                Settings::self()->leaveOnServerSize() >= 1 : false);
+
+    // We need to fetch the collection, as the CollectionRequester needs the name
+    // of it to work correctly
+    Collection targetCollection(Settings::self()->targetCollection());
+    if (targetCollection.isValid()) {
+        CollectionFetchJob *fetchJob = new CollectionFetchJob(targetCollection,
+                CollectionFetchJob::Base,
+                this);
+        connect(fetchJob, &CollectionFetchJob::collectionsReceived, this, &AccountDialog::targetCollectionReceived);
+    } else {
+        // FIXME: This is a bit duplicated from POP3Resource...
+
+        // No target collection set in the config? Try requesting a default inbox
+        SpecialMailCollectionsRequestJob *requestJob = new SpecialMailCollectionsRequestJob(this);
+        requestJob->requestDefaultCollection(SpecialMailCollections::Inbox);
+        requestJob->start();
+        connect(requestJob, &SpecialMailCollectionsRequestJob::result, this, &AccountDialog::localFolderRequestJobFinished);
+    }
+
+    mWallet = Wallet::openWallet(Wallet::NetworkWallet(), winId(),
+                                 Wallet::Asynchronous);
+    if (mWallet) {
+        connect(mWallet, &KWallet::Wallet::walletOpened, this, &AccountDialog::walletOpenedForLoading);
+    } else {
+        passwordEdit->setPlaceholderText(i18n("Wallet disabled in system settings"));
+    }
+    passwordEdit->setEnabled(false);
+    passwordLabel->setEnabled(false);
+}
+
+void AccountDialog::walletOpenedForLoading(bool success)
+{
+    if (success) {
+        if (mWallet->isOpen()) {
+            passwordEdit->setEnabled(true);
+            passwordLabel->setEnabled(true);
+        }
+        if (mWallet->isOpen() && mWallet->hasFolder(QStringLiteral("pop3"))) {
+            QString password;
+            mWallet->setFolder(QStringLiteral("pop3"));
+            mWallet->readPassword(mParentResource->identifier(), password);
+            passwordEdit->setText(password);
+            mInitalPassword = password;
+        } else {
+            qCWarning(POP3RESOURCE_LOG) << "Wallet not open or doesn't have pop3 folder.";
+        }
+    } else {
+        qCWarning(POP3RESOURCE_LOG) << "Failed to open wallet for loading the password.";
+    }
+
+    const bool walletError = !success || !mWallet->isOpen();
+    if (walletError) {
+        passwordEdit->setPlaceholderText(i18n("Unable to open wallet"));
+    }
+}
+
+void AccountDialog::walletOpenedForSaving(bool success)
+{
+    if (success) {
+        if (mWallet && mWallet->isOpen()) {
+
+            // Remove the password from the wallet if the user doesn't want to store it
+            if (passwordEdit->text().isEmpty() && mWallet->hasFolder(QStringLiteral("pop3"))) {
+                mWallet->setFolder(QStringLiteral("pop3"));
+                mWallet->removeEntry(mParentResource->identifier());
+            }
+
+            // Store the password in the wallet if the user wants that
+            else if (!passwordEdit->text().isEmpty()) {
+                if (!mWallet->hasFolder(QStringLiteral("pop3"))) {
+                    mWallet->createFolder(QStringLiteral("pop3"));
+                }
+                mWallet->setFolder(QStringLiteral("pop3"));
+                mWallet->writePassword(mParentResource->identifier(), passwordEdit->text());
+            }
+
+            mParentResource->clearCachedPassword();
+        } else {
+            qCWarning(POP3RESOURCE_LOG) << "Wallet not open.";
+        }
+    } else {
+        // Should we alert the user here?
+        qCWarning(POP3RESOURCE_LOG) << "Failed to open wallet for saving the password.";
+    }
+
+    delete mWallet;
+    mWallet = Q_NULLPTR;
+    accept();
+}
+
+void AccountDialog::slotLeaveOnServerClicked()
+{
+    const bool state = leaveOnServerCheck->isChecked();
+    leaveOnServerDaysCheck->setEnabled(state);
+    leaveOnServerCountCheck->setEnabled(state);
+    leaveOnServerSizeCheck->setEnabled(state);
+    if (state) {
+        if (leaveOnServerDaysCheck->isChecked()) {
+            slotEnableLeaveOnServerDays(state);
+        }
+        if (leaveOnServerCountCheck->isChecked()) {
+            slotEnableLeaveOnServerCount(state);
+        }
+        if (leaveOnServerSizeCheck->isChecked()) {
+            slotEnableLeaveOnServerSize(state);
+        }
+    } else {
+        slotEnableLeaveOnServerDays(state);
+        slotEnableLeaveOnServerCount(state);
+        slotEnableLeaveOnServerSize(state);
+    }
+    if (mServerTest && !mServerTest->capabilities().contains(ServerTest::UIDL) &&
+            leaveOnServerCheck->isChecked()) {
+        KMessageBox::information(topLevelWidget(),
+                                 i18n("The server does not seem to support unique "
+                                      "message numbers, but this is a "
+                                      "requirement for leaving messages on the "
+                                      "server.\n"
+                                      "Since some servers do not correctly "
+                                      "announce their capabilities you still "
+                                      "have the possibility to turn leaving "
+                                      "fetched messages on the server on."));
+    }
+}
+
+void AccountDialog::slotFilterOnServerClicked()
+{
+    if (mServerTest && !mServerTest->capabilities().contains(ServerTest::Top) &&
+            filterOnServerCheck->isChecked()) {
+        KMessageBox::information(topLevelWidget(),
+                                 i18n("The server does not seem to support "
+                                      "fetching message headers, but this is a "
+                                      "requirement for filtering messages on the "
+                                      "server.\n"
+                                      "Since some servers do not correctly "
+                                      "announce their capabilities you still "
+                                      "have the possibility to turn filtering "
+                                      "messages on the server on."));
+    }
+}
+
+void AccountDialog::slotPipeliningClicked()
+{
+    if (usePipeliningCheck->isChecked())
+        KMessageBox::information(topLevelWidget(),
+                                 i18n("Please note that this feature can cause some POP3 servers "
+                                      "that do not support pipelining to send corrupted mail;\n"
+                                      "this is configurable, though, because some servers support pipelining "
+                                      "but do not announce their capabilities. To check whether your POP3 server "
+                                      "announces pipelining support use the \"Auto Detect\""
+                                      " button at the bottom of the dialog;\n"
+                                      "if your server does not announce it, but you want more speed, then "
+                                      "you should do some testing first by sending yourself a batch "
+                                      "of mail and downloading it."), QString(),
+                                 QStringLiteral("pipelining"));
+}
+
+void AccountDialog::slotPopEncryptionChanged(int id)
+{
+    qCDebug(POP3RESOURCE_LOG) << "setting port";
+    // adjust port
+    if (id == Transport::EnumEncryption::SSL || portEdit->value() == 995) {
+        portEdit->setValue((id == Transport::EnumEncryption::SSL) ? 995 : 110);
+    }
+
+    qCDebug(POP3RESOURCE_LOG) << "port set ";
+    enablePopFeatures(); // removes invalid auth options from the combobox
+}
+
+void AccountDialog::slotCheckPopCapabilities()
+{
+    if (hostEdit->text().isEmpty()) {
+        KMessageBox::sorry(this, i18n("Please specify a server and port on "
+                                      "the General tab first."));
+        return;
+    }
+    delete mServerTest;
+    mServerTest = new ServerTest(this);
+    BusyCursorHelper *busyCursorHelper = new BusyCursorHelper(mServerTest);
+    mServerTest->setProgressBar(checkCapabilitiesProgress);
+    mOkButton->setEnabled(false);
+    checkCapabilitiesStack->setCurrentIndex(1);
+    Transport::EnumEncryption::type encryptionType;
+    if (encryptionSSL->isChecked()) {
+        encryptionType = Transport::EnumEncryption::SSL;
+    } else {
+        encryptionType = Transport::EnumEncryption::None;
+    }
+    mServerTest->setPort(encryptionType, portEdit->value());
+    mServerTest->setServer(hostEdit->text());
+    mServerTest->setProtocol(QStringLiteral("pop"));
+    connect(mServerTest, &MailTransport::ServerTest::finished, this, &AccountDialog::slotPopCapabilities);
+    connect(mServerTest, &MailTransport::ServerTest::finished, busyCursorHelper, &BusyCursorHelper::deleteLater);
+
+    mServerTest->start();
+    mServerTestFailed = false;
+}
+
+void AccountDialog::slotPopCapabilities(const QList<int> &encryptionTypes)
+{
+    checkCapabilitiesStack->setCurrentIndex(0);
+    mOkButton->setEnabled(true);
+
+    // if both fail, popup a dialog
+    if (!mServerTest->isNormalPossible() && !mServerTest->isSecurePossible()) {
+        KMessageBox::sorry(this, i18n("Unable to connect to the server, please verify the server address."));
+    }
+
+    // If the servertest did not find any useable authentication modes, assume the
+    // connection failed and don't disable any of the radioboxes.
+    if (encryptionTypes.isEmpty()) {
+        mServerTestFailed = true;
+        return;
+    }
+
+    encryptionNone->setEnabled(encryptionTypes.contains(Transport::EnumEncryption::None));
+    encryptionSSL->setEnabled(encryptionTypes.contains(Transport::EnumEncryption::SSL));
+    encryptionTLS->setEnabled(encryptionTypes.contains(Transport::EnumEncryption::TLS));
+
+    usePipeliningCheck->setChecked(mServerTest->capabilities().contains(ServerTest::Pipelining));
+
+    checkHighest(encryptionButtonGroup);
+}
+
+void AccountDialog::enablePopFeatures()
+{
+    if (!mServerTest || mServerTestFailed) {
+        return;
+    }
+
+    QList<int> supportedAuths;
+    if (encryptionButtonGroup->checkedId() == Transport::EnumEncryption::None) {
+        supportedAuths = mServerTest->normalProtocols();
+    }
+    if (encryptionButtonGroup->checkedId() == Transport::EnumEncryption::SSL) {
+        supportedAuths = mServerTest->secureProtocols();
+    }
+    if (encryptionButtonGroup->checkedId() == Transport::EnumEncryption::TLS) {
+        supportedAuths = mServerTest->tlsProtocols();
+    }
+
+    authCombo->clear();
+    foreach (int prot, supportedAuths) {
+        authCombo->addItem(Transport::authenticationTypeString(prot), prot);
+    }
+
+    if (mServerTest && !mServerTest->capabilities().contains(ServerTest::Pipelining) &&
+            usePipeliningCheck->isChecked()) {
+        usePipeliningCheck->setChecked(false);
+        KMessageBox::information(topLevelWidget(),
+                                 i18n("The server does not seem to support "
+                                      "pipelining; therefore, this option has "
+                                      "been disabled.\n"
+                                      "Since some servers do not correctly "
+                                      "announce their capabilities you still "
+                                      "have the possibility to turn pipelining "
+                                      "on. But please note that this feature can "
+                                      "cause some POP servers that do not "
+                                      "support pipelining to send corrupt "
+                                      "messages. So before using this feature "
+                                      "with important mail you should first "
+                                      "test it by sending yourself a larger "
+                                      "number of test messages which you all "
+                                      "download in one go from the POP "
+                                      "server."));
+    }
+
+    if (mServerTest && !mServerTest->capabilities().contains(ServerTest::UIDL) &&
+            leaveOnServerCheck->isChecked()) {
+        leaveOnServerCheck->setChecked(false);
+        KMessageBox::information(topLevelWidget(),
+                                 i18n("The server does not seem to support unique "
+                                      "message numbers, but this is a "
+                                      "requirement for leaving messages on the "
+                                      "server; therefore, this option has been "
+                                      "disabled.\n"
+                                      "Since some servers do not correctly "
+                                      "announce their capabilities you still "
+                                      "have the possibility to turn leaving "
+                                      "fetched messages on the server on."));
+    }
+
+    if (mServerTest && !mServerTest->capabilities().contains(ServerTest::Top) &&
+            filterOnServerCheck->isChecked()) {
+        filterOnServerCheck->setChecked(false);
+        KMessageBox::information(topLevelWidget(),
+                                 i18n("The server does not seem to support "
+                                      "fetching message headers, but this is a "
+                                      "requirement for filtering messages on the "
+                                      "server; therefore, this option has been "
+                                      "disabled.\n"
+                                      "Since some servers do not correctly "
+                                      "announce their capabilities you still "
+                                      "have the possibility to turn filtering "
+                                      "messages on the server on."));
+    }
+}
+
+static void addAuthenticationItem(QComboBox *combo,
+                                  int authenticationType)
+{
+    combo->addItem(Transport::authenticationTypeString(authenticationType),
+                   QVariant(authenticationType));
+}
+
+void AccountDialog::populateDefaultAuthenticationOptions()
+{
+    authCombo->clear();
+    addAuthenticationItem(authCombo, Transport::EnumAuthenticationType::CLEAR);
+    addAuthenticationItem(authCombo, Transport::EnumAuthenticationType::LOGIN);
+    addAuthenticationItem(authCombo, Transport::EnumAuthenticationType::PLAIN);
+    addAuthenticationItem(authCombo, Transport::EnumAuthenticationType::CRAM_MD5);
+    addAuthenticationItem(authCombo, Transport::EnumAuthenticationType::DIGEST_MD5);
+    addAuthenticationItem(authCombo, Transport::EnumAuthenticationType::NTLM);
+    addAuthenticationItem(authCombo, Transport::EnumAuthenticationType::GSSAPI);
+    addAuthenticationItem(authCombo, Transport::EnumAuthenticationType::APOP);
+}
+
+void AccountDialog::slotLeaveOnServerDaysChanged(int value)
+{
+    leaveOnServerDaysSpin->setSuffix(i18np(" day", " days", value));
+}
+
+void AccountDialog::slotLeaveOnServerCountChanged(int value)
+{
+    leaveOnServerCountSpin->setSuffix(i18np(" message", " messages", value));
+}
+
+void AccountDialog::slotFilterOnServerSizeChanged(int value)
+{
+    filterOnServerSizeSpin->setSuffix(i18np(" byte", " bytes", value));
+}
+
+void AccountDialog::checkHighest(QButtonGroup *btnGroup)
+{
+    QListIterator<QAbstractButton *> it(btnGroup->buttons());
+    it.toBack();
+    while (it.hasPrevious()) {
+        QAbstractButton *btn = it.previous();
+        if (btn && btn->isEnabled()) {
+            btn->animateClick();
+            return;
+        }
+    }
+}
+
+void AccountDialog::slotAccepted()
+{
+    saveSettings();
+    // Don't call accept() yet, saveSettings() triggers an asnychronous wallet operation,
+    // which will call accept() when it is finished
+}
+
+void AccountDialog::saveSettings()
+{
+    mParentResource->setName(nameEdit->text());
+
+    Settings::self()->setIntervalCheckEnabled(intervalCheck->checkState() == Qt::Checked);
+    Settings::self()->setIntervalCheckInterval(intervalSpin->value());
+    Settings::self()->setHost(hostEdit->text().trimmed());
+    Settings::self()->setPort(portEdit->value());
+    Settings::self()->setLogin(loginEdit->text().trimmed());
+    Settings::self()->setPrecommand(precommand->text());
+    Settings::self()->setUseSSL(encryptionSSL->isChecked());
+    Settings::self()->setUseTLS(encryptionTLS->isChecked());
+    Settings::self()->setAuthenticationMethod(authCombo->itemData(authCombo->currentIndex()).toInt());
+    Settings::self()->setPipelining(usePipeliningCheck->isChecked());
+    Settings::self()->setLeaveOnServer(leaveOnServerCheck->isChecked());
+    Settings::self()->setLeaveOnServerDays(leaveOnServerCheck->isChecked() ?
+                                           (leaveOnServerDaysCheck->isChecked() ?
+                                            leaveOnServerDaysSpin->value() : -1) : 0);
+    Settings::self()->setLeaveOnServerCount(leaveOnServerCheck->isChecked() ?
+                                            (leaveOnServerCountCheck->isChecked() ?
+                                                    leaveOnServerCountSpin->value() : -1) : 0);
+    Settings::self()->setLeaveOnServerSize(leaveOnServerCheck->isChecked() ?
+                                           (leaveOnServerSizeCheck->isChecked() ?
+                                            leaveOnServerSizeSpin->value() : -1) : 0);
+    Settings::self()->setFilterOnServer(filterOnServerCheck->isChecked());
+    Settings::self()->setFilterCheckSize(filterOnServerSizeSpin->value());
+    Settings::self()->setTargetCollection(folderRequester->collection().id());
+    Settings::self()->save();
+
+    // Now, either save the password or delete it from the wallet. For both, we need
+    // to open it.
+    const bool userChangedPassword = mInitalPassword != passwordEdit->text();
+    const bool userWantsToDeletePassword =
+        passwordEdit->text().isEmpty() && userChangedPassword;
+
+    if ((!passwordEdit->text().isEmpty() && userChangedPassword) ||
+            userWantsToDeletePassword) {
+        qCDebug(POP3RESOURCE_LOG) << mWallet <<  mWallet->isOpen();
+        if (mWallet && mWallet->isOpen()) {
+            // wallet is already open
+            walletOpenedForSaving(true);
+        } else {
+            // we need to open the wallet
+            qCDebug(POP3RESOURCE_LOG) << "we need to open the wallet";
+            mWallet = Wallet::openWallet(Wallet::NetworkWallet(), winId(),
+                                         Wallet::Asynchronous);
+            if (mWallet) {
+                connect(mWallet, &KWallet::Wallet::walletOpened, this, &AccountDialog::walletOpenedForSaving);
+            } else {
+                accept();
+            }
+        }
+    } else {
+        accept();
+    }
+}
+
+void AccountDialog::slotEnableLeaveOnServerDays(bool state)
+{
+    if (state && !leaveOnServerDaysCheck->isEnabled()) {
+        return;
+    }
+    leaveOnServerDaysSpin->setEnabled(state);
+}
+
+void AccountDialog::slotEnableLeaveOnServerCount(bool state)
+{
+    if (state && !leaveOnServerCountCheck->isEnabled()) {
+        return;
+    }
+    leaveOnServerCountSpin->setEnabled(state);
+}
+
+void AccountDialog::slotEnableLeaveOnServerSize(bool state)
+{
+    if (state && !leaveOnServerSizeCheck->isEnabled()) {
+        return;
+    }
+    leaveOnServerSizeSpin->setEnabled(state);
+}
+
+void AccountDialog::slotEnablePopInterval(bool state)
+{
+    intervalSpin->setEnabled(state);
+    intervalLabel->setEnabled(state);
+}
+
+void AccountDialog::targetCollectionReceived(Akonadi::Collection::List collections)
+{
+    folderRequester->setCollection(collections.first());
+}
+
+void AccountDialog::localFolderRequestJobFinished(KJob *job)
+{
+    if (!job->error()) {
+        Collection targetCollection = SpecialMailCollections::self()->defaultCollection(SpecialMailCollections::Inbox);
+        Q_ASSERT(targetCollection.isValid());
+        folderRequester->setCollection(targetCollection);
+    }
+}
+
diff --git a/resources/pop3/accountdialog.h b/resources/pop3/accountdialog.h
new file mode 100644 (file)
index 0000000..f3a298e
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ *   Copyright (C) 2000 Espen Sand, espen@kde.org
+ *   Copyright 2009 Thomas McGuire <mcguire@kde.org>
+ *   Copyright (c) 2009-2010 Klaralvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
+ *   Copyright (C) 2010 Casey Link <unnamedrambler@gmail.com>
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ */
+
+#ifndef _ACCOUNT_DIALOG_H_
+#define _ACCOUNT_DIALOG_H_
+
+#include "ui_popsettings.h"
+
+namespace MailTransport
+{
+class ServerTest;
+}
+namespace KWallet
+{
+class Wallet;
+}
+
+class POP3Resource;
+class KJob;
+
+class AccountDialog : public QDialog, private Ui::PopPage
+{
+    Q_OBJECT
+
+public:
+    AccountDialog(POP3Resource *parentResource, WId parentWindow);
+    virtual ~AccountDialog();
+
+private Q_SLOTS:
+    void slotEnablePopInterval(bool state);
+    void slotLeaveOnServerClicked();
+    void slotEnableLeaveOnServerDays(bool state);
+    void slotEnableLeaveOnServerCount(bool state);
+    void slotEnableLeaveOnServerSize(bool state);
+    void slotFilterOnServerClicked();
+    void slotPipeliningClicked();
+    void slotPopEncryptionChanged(int);
+    void slotCheckPopCapabilities();
+    void slotPopCapabilities(const QList<int> &);
+    void slotLeaveOnServerDaysChanged(int value);
+    void slotLeaveOnServerCountChanged(int value);
+    void slotFilterOnServerSizeChanged(int value);
+
+    void targetCollectionReceived(Akonadi::Collection::List collections);
+    void localFolderRequestJobFinished(KJob *job);
+    void walletOpenedForLoading(bool success);
+    void walletOpenedForSaving(bool success);
+    void slotAccepted();
+private:
+    void setupWidgets();
+    void loadSettings();
+    void saveSettings();
+    void checkHighest(QButtonGroup *);
+    void enablePopFeatures();
+    void populateDefaultAuthenticationOptions();
+
+private:
+    POP3Resource *mParentResource;
+    QButtonGroup *encryptionButtonGroup;
+    MailTransport::ServerTest *mServerTest;
+    QRegExpValidator mValidator;
+    bool mServerTestFailed;
+    KWallet::Wallet *mWallet;
+    QString mInitalPassword;
+    QPushButton *mOkButton;
+};
+
+#endif
diff --git a/resources/pop3/autotests/CMakeLists.txt b/resources/pop3/autotests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..c53ec01
--- /dev/null
@@ -0,0 +1,53 @@
+set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} )
+
+include_directories(
+    ${CMAKE_CURRENT_SOURCE_DIR}
+    ${CMAKE_CURRENT_SOURCE_DIR}
+)
+
+# Stolen from kdepimlibs/akonadi/tests
+kde_enable_exceptions()
+
+macro(add_akonadi_isolated_test _source)
+  get_filename_component(_targetName ${_source} NAME_WE)
+  set(_srcList ${_source} fakeserver/fakeserver.cpp)
+
+  kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/../settings.kcfg org.kde.Akonadi.POP3.Settings)
+  set(pop3settingsinterface_xml ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.POP3.Settings.xml)
+  kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/../../maildir/maildirresource.kcfg org.kde.Akonadi.Maildir.Settings)
+  set(maildirsettingsinterface_xml ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.Maildir.Settings.xml)
+  set_source_files_properties(${pop3settingsinterface_xml} PROPERTIES INCLUDE "../metatype.h")
+  qt5_add_dbus_interface(_srcList ${pop3settingsinterface_xml} pop3settings)
+  qt5_add_dbus_interface(_srcList ${maildirsettingsinterface_xml} maildirsettings)
+  # add the dbus interace to every test (easier than adding to particular tests only)
+  #qt5_add_dbus_interface(_srcList ../org.kde.krss.xml krssinterface)
+
+  add_executable(${_targetName} ${_srcList})
+  target_link_libraries(${_targetName}
+    Qt5::Test
+    KF5::AkonadiCore
+    KF5::Mime
+    Qt5::Network
+    Qt5::Widgets
+    Qt5::DBus
+  )
+
+  # based on kde4_add_unit_test
+  if (WIN32)
+    get_target_property( _loc ${_targetName} LOCATION )
+    set(_executable ${_loc}.bat)
+  else ()
+    set(_executable ${EXECUTABLE_OUTPUT_PATH}/${_targetName})
+  endif ()
+  if (UNIX)
+    set(_executable ${_executable}.shell)
+  endif ()
+
+  find_program(_testrunner akonaditest)
+
+  if (KDEPIM_RUN_ISOLATED_TESTS)
+    add_test( ${_targetName} ${_testrunner} -c ${CMAKE_CURRENT_SOURCE_DIR}/unittestenv/config.xml ${_executable} )
+  endif ()
+endmacro(add_akonadi_isolated_test)
+
+add_akonadi_isolated_test(pop3test.cpp)
diff --git a/resources/pop3/autotests/fakeserver/fakeserver.cpp b/resources/pop3/autotests/fakeserver/fakeserver.cpp
new file mode 100644 (file)
index 0000000..0136075
--- /dev/null
@@ -0,0 +1,293 @@
+/*
+   Copyright (C) 2008 Omat Holding B.V. <info@omat.nl>
+   Copyright (C) 2009 Thomas McGuire <mcguire@kde.org>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+// Own
+#include "fakeserver.h"
+
+// Qt
+#include <QDebug>
+#include <QTcpServer>
+
+FakeServerThread::FakeServerThread(QObject *parent)
+    : QThread(parent),
+      mServer(Q_NULLPTR)
+{
+}
+
+void FakeServerThread::run()
+{
+    mServer = new FakeServer();
+
+    // Run forever, until someone from the outside calls quit() on us and quits the
+    // event loop
+    exec();
+
+    delete mServer;
+    mServer = Q_NULLPTR;
+}
+
+FakeServer *FakeServerThread::server() const
+{
+    Q_ASSERT(mServer != 0);
+    return mServer;
+}
+
+FakeServer::FakeServer(QObject *parent)
+    : QObject(parent),
+      mConnections(0),
+      mProgress(0),
+      mGotDisconnected(false)
+{
+    mTcpServer = new QTcpServer();
+    if (!mTcpServer->listen(QHostAddress(QHostAddress::LocalHost), 5989)) {
+        qCritical() << "Unable to start the server";
+    }
+
+    connect(mTcpServer, &QTcpServer::newConnection, this, &FakeServer::newConnection);
+}
+
+FakeServer::~FakeServer()
+{
+    if (mConnections > 0) {
+        disconnect(mTcpServerConnection, &QTcpSocket::readyRead, this, &FakeServer::dataAvailable);
+    }
+
+    delete mTcpServer;
+    mTcpServer = Q_NULLPTR;
+}
+
+QByteArray FakeServer::parseDeleteMark(const QByteArray &expectedData,
+                                       const QByteArray &dataReceived)
+{
+    // Only called from parseResponse(), which is already thread-safe
+
+    const QByteArray deleteMark = QStringLiteral("%DELE%").toUtf8();
+    if (expectedData.contains(deleteMark)) {
+        Q_ASSERT(!mAllowedDeletions.isEmpty());
+        for (int i = 0; i < mAllowedDeletions.size(); i++) {
+            QByteArray substituted = expectedData;
+            substituted = substituted.replace(deleteMark, mAllowedDeletions[i]);
+            if (substituted == dataReceived) {
+                mAllowedDeletions.removeAt(i);
+                return substituted;
+            }
+        }
+        qWarning() << "Received:" << dataReceived.data()
+                   << "\nExpected:" << expectedData.data();
+        Q_ASSERT_X(false, "FakeServer::parseDeleteMark", "Unable to substitute data!");
+        return QByteArray();
+    } else {
+        return expectedData;
+    }
+}
+
+QByteArray FakeServer::parseRetrMark(const QByteArray &expectedData,
+                                     const QByteArray &dataReceived)
+{
+    // Only called from parseResponse(), which is already thread-safe
+
+    const QByteArray retrMark = QStringLiteral("%RETR%").toUtf8();
+    if (expectedData.contains(retrMark)) {
+        Q_ASSERT(!mAllowedRetrieves.isEmpty());
+        for (int i = 0; i < mAllowedRetrieves.size(); i++) {
+            QByteArray substituted = expectedData;
+            substituted = substituted.replace(retrMark, mAllowedRetrieves[i]);
+            if (substituted == dataReceived) {
+                mAllowedRetrieves.removeAt(i);
+                return substituted;
+            }
+        }
+        qWarning() << "Received:" << dataReceived.data()
+                   << "\nExpected:" << expectedData.data();
+        Q_ASSERT_X(false, "FakeServer::parseRetrMark", "Unable to substitute data!");
+        return QByteArray();
+    } else {
+        return expectedData;
+    }
+}
+
+QByteArray FakeServer::parseResponse(const QByteArray &expectedData,
+                                     const QByteArray &dataReceived)
+{
+    // Only called from dataAvailable, which is already thread-safe
+
+    QByteArray result;
+    result = parseDeleteMark(expectedData, dataReceived);
+    if (result != expectedData) {
+        return result;
+    } else {
+        return parseRetrMark(expectedData, dataReceived);
+    }
+}
+
+/*
+// Used only for the debug output in dataAvailable()
+static QByteArray removeCRLF( const QByteArray &ba )
+{
+  QByteArray returnArray = ba;
+  returnArray.replace( "\r\n", QByteArray() );
+  return returnArray;
+}
+*/
+
+void FakeServer::dataAvailable()
+{
+    QMutexLocker locker(&mMutex);
+    mProgress++;
+
+    // We got data, so we better expect it and have an answer!
+    Q_ASSERT(!mReadData.isEmpty());
+    Q_ASSERT(!mWriteData.isEmpty());
+
+    const QByteArray data = mTcpServerConnection->readAll();
+    //qDebug() << "Got data:" << removeCRLF( data );
+    const QByteArray expected(mReadData.takeFirst());
+    //qDebug() << "Expected data:" << removeCRLF( expected );
+    const QByteArray reallyExpected = parseResponse(expected, data);
+    //qDebug() << "Really expected:" << removeCRLF( reallyExpected );
+
+    Q_ASSERT(data == reallyExpected);
+
+    QByteArray toWrite = mWriteData.takeFirst();
+    //qDebug() << "Going to write data:" << removeCRLF( toWrite );
+    const bool allWritten = mTcpServerConnection->write(toWrite) == toWrite.size();
+    Q_ASSERT(allWritten);
+    Q_UNUSED(allWritten);
+    const bool flushed = mTcpServerConnection->flush();
+    Q_ASSERT(flushed);
+    Q_UNUSED(flushed);
+}
+
+void FakeServer::newConnection()
+{
+    QMutexLocker locker(&mMutex);
+    Q_ASSERT(mConnections == 0);
+    mConnections++;
+    mGotDisconnected = false;
+
+    mTcpServerConnection = mTcpServer->nextPendingConnection();
+    mTcpServerConnection->write(QByteArray("+OK Initech POP3 server ready.\r\n"));
+    connect(mTcpServerConnection, &QTcpSocket::readyRead, this, &FakeServer::dataAvailable);
+    connect(mTcpServerConnection, &QTcpSocket::disconnected, this, &FakeServer::slotDisconnected);
+}
+
+void FakeServer::slotDisconnected()
+{
+    QMutexLocker locker(&mMutex);
+    mConnections--;
+    mGotDisconnected = true;
+    Q_ASSERT(mConnections == 0);
+    Q_ASSERT(mAllowedDeletions.isEmpty());
+    Q_ASSERT(mAllowedRetrieves.isEmpty());
+    Q_ASSERT(mReadData.isEmpty());
+    Q_ASSERT(mWriteData.isEmpty());
+    Q_EMIT disconnected();
+}
+
+void FakeServer::setAllowedDeletions(const QString &deleteIds)
+{
+    QMutexLocker locker(&mMutex);
+    mAllowedDeletions.clear();
+    QStringList ids = deleteIds.split(QLatin1Char(','), QString::SkipEmptyParts);
+    foreach (const QString &id, ids) {
+        mAllowedDeletions.append(id.toUtf8());
+    }
+}
+
+void FakeServer::setAllowedRetrieves(const QString &retrieveIds)
+{
+    QMutexLocker locker(&mMutex);
+    mAllowedRetrieves.clear();
+    QStringList ids = retrieveIds.split(QLatin1Char(','), QString::SkipEmptyParts);
+    foreach (const QString &id, ids) {
+        mAllowedRetrieves.append(id.toUtf8());
+    }
+}
+
+void FakeServer::setMails(const QList<QByteArray> &mails)
+{
+    QMutexLocker locker(&mMutex);
+    mMails = mails;
+}
+
+void FakeServer::setNextConversation(const QString &conversation,
+                                     const QList<int> &exceptions)
+{
+    QMutexLocker locker(&mMutex);
+
+    Q_ASSERT(mReadData.isEmpty());
+    Q_ASSERT(mWriteData.isEmpty());
+    Q_ASSERT(!conversation.isEmpty());
+
+    mGotDisconnected = false;
+    QStringList lines = conversation.split(QStringLiteral("\r\n"), QString::SkipEmptyParts);
+    Q_ASSERT(lines.first().startsWith(QLatin1String("C:")));
+
+    enum Mode { Client, Server };
+    Mode mode = Client;
+
+    const QByteArray mailSizeMarker = QStringLiteral("%MAILSIZE%").toLatin1();
+    const QByteArray mailMarker = QStringLiteral("%MAIL%").toLatin1();
+    int sizeIndex = 0;
+    int mailIndex = 0;
+
+    foreach (const QString &line, lines) {
+
+        QByteArray lineData(line.toUtf8());
+
+        if (lineData.contains(mailSizeMarker)) {
+            Q_ASSERT(mMails.size() > sizeIndex);
+            lineData.replace(mailSizeMarker,
+                             QString::number(mMails[sizeIndex++].size()).toLatin1());
+        }
+        if (lineData.contains(mailMarker)) {
+            while (exceptions.contains(mailIndex + 1)) {
+                mailIndex++;
+            }
+            Q_ASSERT(mMails.size() > mailIndex);
+            lineData.replace(mailMarker, mMails[mailIndex++]);
+        }
+
+        if (lineData.startsWith("S: ")) {
+            mWriteData.append(lineData.mid(3) + "\r\n");
+            mode = Server;
+        } else if (line.startsWith(QLatin1String("C: "))) {
+            mReadData.append(lineData.mid(3) + "\r\n");
+            mode = Client;
+        } else {
+            switch (mode) {
+            case Server: mWriteData.last() += (lineData + "\r\n"); break;
+            case Client: mReadData.last() += (lineData + "\r\n"); break;
+            }
+        }
+    }
+}
+
+int FakeServer::progress() const
+{
+    QMutexLocker locker(&mMutex);
+    return mProgress;
+}
+
+bool FakeServer::gotDisconnected() const
+{
+    QMutexLocker locker(&mMutex);
+    return mGotDisconnected;
+}
+
diff --git a/resources/pop3/autotests/fakeserver/fakeserver.h b/resources/pop3/autotests/fakeserver/fakeserver.h
new file mode 100644 (file)
index 0000000..fa9579e
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+   Copyright (C) 2008 Omat Holding B.V. <info@omat.nl>
+   Copyright (C) 2009 Thomas McGuire <mcguire@kde.org>
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public
+   License as published by the Free Software Foundation; either
+   version 2 of the License, or (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+#ifndef FAKESERVER_H
+#define FAKESERVER_H
+
+#include <QTcpSocket>
+#include <QTcpServer>
+#include <QThread>
+#include <QMutex>
+
+class FakeServer : public QObject
+{
+    Q_OBJECT
+
+public:
+    FakeServer(QObject *parent = Q_NULLPTR);
+    ~FakeServer();
+
+    void setNextConversation(const QString &conversation,
+                             const QList<int> &exceptions = QList<int>());
+    void setAllowedDeletions(const QString &deleteIds);
+    void setAllowedRetrieves(const QString &retrieveIds);
+    void setMails(const QList<QByteArray> &mails);
+
+    // This is kind of a hack: The POP3 test needs to know when the POP3 client
+    // disconnects from the server. Normally, we could just use a QSignalSpy on the
+    // disconnected() signal, but that is not thread-safe. Therefore this hack with the
+    // state variable mGotDisconnected
+    bool gotDisconnected() const;
+
+    // Returns an integer that is incremented each time the POP3 server receives some
+    // data
+    int progress() const;
+
+Q_SIGNALS:
+    void disconnected();
+
+private Q_SLOTS:
+
+    void newConnection();
+    void dataAvailable();
+    void slotDisconnected();
+
+private:
+
+    QByteArray parseDeleteMark(const QByteArray &expectedData, const QByteArray &dataReceived);
+    QByteArray parseRetrMark(const QByteArray &expectedData, const QByteArray &dataReceived);
+    QByteArray parseResponse(const QByteArray &expectedData, const QByteArray &dataReceived);
+
+    QList<QByteArray> mReadData;
+    QList<QByteArray> mWriteData;
+    QList<QByteArray> mAllowedDeletions;
+    QList<QByteArray> mAllowedRetrieves;
+    QList<QByteArray> mMails;
+    QTcpServer *mTcpServer;
+    QTcpSocket *mTcpServerConnection;
+    int mConnections;
+    int mProgress;
+    bool mGotDisconnected;
+
+    // We use one big mutex to protect everything
+    // There shouldn't be deadlocks, as there are only 2 places where the functions
+    // are called: From the KTcpSocket signals, which are triggered by the POP3 ioslave,
+    //             and from the actual test.
+    mutable QMutex mMutex;
+};
+
+class FakeServerThread : public QThread
+{
+    Q_OBJECT
+
+public:
+
+    explicit FakeServerThread(QObject *parent);
+    void run() Q_DECL_OVERRIDE;
+
+    // Returns the FakeServer use. Be careful when using this and make sure
+    // the methods you use are actually thread-safe!!
+    // This should however be the case because the FakeServer uses one big mutex
+    // to protect everything.
+    FakeServer *server() const;
+
+private:
+
+    FakeServer *mServer;
+};
+
+#endif
+
diff --git a/resources/pop3/autotests/pop3test.cpp b/resources/pop3/autotests/pop3test.cpp
new file mode 100644 (file)
index 0000000..762aac8
--- /dev/null
@@ -0,0 +1,916 @@
+/* Copyright 2009 Thomas McGuire <mcguire@kde.org>
+
+   This library is free software; you can redistribute it and/or modify
+   it under the terms of the GNU Library General Public License as published
+   by the Free Software Foundation; either version 2 of the License or
+   ( at your option ) version 3 or, at the discretion of KDE e.V.
+   ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version.
+
+   This library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Library General Public License for more details.
+
+   You should have received a copy of the GNU Library General Public License
+   along with this library; see the file COPYING.LIB.  If not, write to
+   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+   Boston, MA 02110-1301, USA.
+*/
+#include "pop3test.h"
+
+#include <AkonadiCore/AgentInstanceCreateJob>
+#include <AkonadiCore/AgentManager>
+#include <AkonadiCore/Control>
+#include <AkonadiCore/CollectionFetchJob>
+#include <AkonadiCore/ItemDeleteJob>
+#include <AkonadiCore/ItemFetchJob>
+#include <AkonadiCore/ItemFetchScope>
+#include <qtest_akonadi.h>
+#include <KMime/Message>
+
+#include <sys/signal.h>
+#include <QStandardPaths>
+
+QTEST_AKONADIMAIN(Pop3Test)
+
+using namespace Akonadi;
+
+void Pop3Test::initTestCase()
+{
+    QVERIFY(Akonadi::Control::start());
+
+    // switch all resources offline to reduce interference from them
+    foreach (Akonadi::AgentInstance agent, Akonadi::AgentManager::self()->instances()) {
+        agent.setIsOnline(false);
+    }
+
+    /*
+    qDebug() << "===========================================================";
+    qDebug() << "============ Stopping for debugging =======================";
+    qDebug() << "===========================================================";
+    kill( qApp->applicationPid(), SIGSTOP );
+    */
+
+    //
+    // Create the maildir and pop3 resources
+    //
+    AgentType maildirType = AgentManager::self()->type(QStringLiteral("akonadi_maildir_resource"));
+    AgentInstanceCreateJob *agentCreateJob = new AgentInstanceCreateJob(maildirType);
+    QVERIFY(agentCreateJob->exec());
+    mMaildirIdentifier = agentCreateJob->instance().identifier();
+
+    AgentType popType = AgentManager::self()->type(QStringLiteral("akonadi_pop3_resource"));
+    agentCreateJob = new AgentInstanceCreateJob(popType);
+    QVERIFY(agentCreateJob->exec());
+    mPop3Identifier = agentCreateJob->instance().identifier();
+
+    //
+    // Configure the maildir resource
+    //
+    QString maildirRootPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + QLatin1String("tester");
+    mMaildirPath = maildirRootPath + QLatin1String("/new");
+    mMaildirSettingsInterface = new OrgKdeAkonadiMaildirSettingsInterface(
+        QLatin1String("org.freedesktop.Akonadi.Resource.") + mMaildirIdentifier,
+        QStringLiteral("/Settings"), QDBusConnection::sessionBus(), this);
+    QDBusReply<void> setPathReply = mMaildirSettingsInterface->setPath(maildirRootPath);
+    QVERIFY(setPathReply.isValid());
+    AgentManager::self()->instance(mMaildirIdentifier).reconfigure();
+    QDBusReply<QString> getPathReply = mMaildirSettingsInterface->path();
+    QCOMPARE(getPathReply.value(), maildirRootPath);
+    AgentManager::self()->instance(mMaildirIdentifier).synchronize();
+
+    //
+    // Find the root maildir collection
+    //
+    bool found = false;
+    QTime time;
+    time.start();
+    while (!found) {
+        CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive);
+        QVERIFY(job->exec());
+        Collection::List collections = job->collections();
+        foreach (const Collection &col, collections) {
+            if (col.resource() == AgentManager::self()->instance(mMaildirIdentifier).identifier()) {
+                mMaildirCollection = col;
+                found = true;
+                break;
+            }
+        }
+
+        QVERIFY(time.elapsed() < 10 * 1000);   // maildir should not need more than 10 secs to sync
+    }
+
+    //
+    // Start the fake POP3 server
+    //
+    mFakeServerThread = new FakeServerThread(this);
+    mFakeServerThread->start();
+    QTest::qWait(100);
+    QVERIFY(mFakeServerThread->server() != Q_NULLPTR);
+
+    //
+    // Configure the pop3 resource
+    //
+    mPOP3SettingsInterface = new OrgKdeAkonadiPOP3SettingsInterface(
+        QLatin1String("org.freedesktop.Akonadi.Resource.") + mPop3Identifier,
+        QStringLiteral("/Settings"), QDBusConnection::sessionBus(), this);
+
+    QDBusReply<uint> reply0 = mPOP3SettingsInterface->port();
+    QVERIFY(reply0.isValid());
+    QVERIFY(reply0.value() == 110);
+
+    mPOP3SettingsInterface->setPort(5989);
+    AgentManager::self()->instance(mPop3Identifier).reconfigure();
+    QDBusReply<uint> reply = mPOP3SettingsInterface->port();
+    QVERIFY(reply.isValid());
+    QVERIFY(reply.value() == 5989);
+
+    mPOP3SettingsInterface->setHost(QStringLiteral("localhost"));
+    AgentManager::self()->instance(mPop3Identifier).reconfigure();
+    QDBusReply<QString> reply2 = mPOP3SettingsInterface->host();
+    QVERIFY(reply2.isValid());
+    QVERIFY(reply2.value() == QLatin1String("localhost"));
+    mPOP3SettingsInterface->setLogin(QStringLiteral("HansWurst"));
+    AgentManager::self()->instance(mPop3Identifier).reconfigure();
+    QDBusReply<QString> reply3 = mPOP3SettingsInterface->login();
+    QVERIFY(reply3.isValid());
+    QVERIFY(reply3.value() == QLatin1String("HansWurst"));
+
+    mPOP3SettingsInterface->setUnitTestPassword(QStringLiteral("Geheim"));
+    AgentManager::self()->instance(mPop3Identifier).reconfigure();
+    QDBusReply<QString> reply4 = mPOP3SettingsInterface->unitTestPassword();
+    QVERIFY(reply4.isValid());
+    QVERIFY(reply4.value() == QLatin1String("Geheim"));
+
+    mPOP3SettingsInterface->setTargetCollection(mMaildirCollection.id());
+    AgentManager::self()->instance(mPop3Identifier).reconfigure();
+    QDBusReply<qlonglong> reply5 = mPOP3SettingsInterface->targetCollection();
+    QVERIFY(reply5.isValid());
+    QVERIFY(reply5.value() == mMaildirCollection.id());
+}
+
+void Pop3Test::cleanupTestCase()
+{
+    // Post the "quit" event to the thread's event loop, then wait until the thread
+    // is finished (it finishes when the event loop finishes)
+    QMetaObject::invokeMethod(mFakeServerThread, "quit", Qt::QueuedConnection);
+    if (!mFakeServerThread->wait(10000)) {
+        qWarning() << "The fake server thread has not yet finished, what is wrong!?";
+    }
+}
+
+static const QByteArray simpleMail1 =
+    "From: \"Bill Lumbergh\" <BillLumbergh@initech.com>\r\n"
+    "To: \"Peter Gibbons\" <PeterGibbons@initech.com>\r\n"
+    "Subject: TPS Reports - New Cover Sheets\r\n"
+    "MIME-Version: 1.0\r\n"
+    "Content-Type: text/plain\r\n"
+    "Date: Mon, 23 Mar 2009 18:04:05 +0300\r\n"
+    "\r\n"
+    "Hi, Peter. What's happening? We need to talk about your TPS reports.\r\n";
+
+static const QByteArray simpleMail2 =
+    "From: \"Amy McCorkell\" <yooper@mtao.net>\r\n"
+    "To: gov.palin@yaho.com\r\n"
+    "Subject: HI SARAH\r\n"
+    "MIME-Version: 1.0\r\n"
+    "Content-Type: text/plain\r\n"
+    "Date: Mon, 23 Mar 2009 18:04:05 +0300\r\n"
+    "\r\n"
+    "Hey Sarah,\r\n"
+    "bla bla bla bla bla\r\n";
+
+static const QByteArray simpleMail3 =
+    "From: chunkylover53@aol.com\r\n"
+    "To: tylerdurden@paperstreetsoapcompany.com\r\n"
+    "Subject: ILOVEYOU\r\n"
+    "MIME-Version: 1.0\r\n"
+    "Content-Type: text/plain\r\n"
+    "Date: Mon, 23 Mar 2009 18:04:05 +0300\r\n"
+    "\r\n"
+    "kindly check the attached LOVELETTER coming from me.\r\n";
+
+static const QByteArray simpleMail4 =
+    "From: karl@aol.com\r\n"
+    "To: lenny@aol.com\r\n"
+    "Subject: Who took the donuts?\r\n"
+    "\r\n"
+    "Hi Lenny, do you know who took all the donuts?\r\n";
+
+static const QByteArray simpleMail5 =
+    "From: foo@bar.com\r\n"
+    "To: bar@foo.com\r\n"
+    "Subject: Hello\r\n"
+    "\r\n"
+    "Hello World!!\r\n";
+
+void Pop3Test::cleanupMaildir(Akonadi::Item::List items)
+{
+    // Delete all mails so the maildir is clean for the next test
+    if (!items.isEmpty()) {
+        ItemDeleteJob *job = new ItemDeleteJob(items);
+        QVERIFY(job->exec());
+    }
+
+    QTime time;
+    time.start();
+    int lastCount = -1;
+    forever {
+        qApp->processEvents();
+        QTest::qWait(500);
+        QDir maildir(mMaildirPath);
+        maildir.refresh();
+        int curCount = maildir.entryList(QDir::Files | QDir::NoDotAndDotDot).count();
+
+        // Restart the timer when a mail arrives, as it shows that the maildir resource is
+        // still alive and kicking.
+        if (curCount != lastCount) {
+            time.restart();
+            lastCount = curCount;
+        }
+
+        if (curCount == 0) {
+            break;
+        }
+
+        QVERIFY(time.elapsed() < 60000 || time.elapsed() > 80000000);
+    }
+}
+
+void Pop3Test::checkMailsInMaildir(const QList<QByteArray> &mails)
+{
+    // Now, test that all mails actually ended up in the maildir. Since the maildir resource
+    // might be slower, give it a timeout so it can write the files to disk
+    QTime time;
+    time.start();
+    int lastCount = -1;
+    forever {
+        qApp->processEvents();
+        QTest::qWait(500);
+        QDir maildir(mMaildirPath);
+        maildir.refresh();
+        int curCount = static_cast<int>(maildir.entryList(QDir::Files | QDir::NoDotAndDotDot).count());
+
+        // Restart the timer when a mail arrives, as it shows that the maildir resource is
+        // still alive and kicking.
+        if (curCount != lastCount) {
+            time.start();
+            lastCount = curCount;
+        }
+
+        if (curCount == mails.count()) {
+            break;
+        }
+        QVERIFY(static_cast<int>(maildir.entryList(QDir::NoDotAndDotDot).count()) <= mails.count());
+        QVERIFY(time.elapsed() < 60000 || time.elapsed() > 80000000);
+    }
+
+    // TODO: check file contents as well or is this overkill?
+}
+
+Akonadi::Item::List Pop3Test::checkMailsOnAkonadiServer(const QList<QByteArray> &mails)
+{
+    // The fake server got disconnected, which means the pop3 resource has entered the QUIT
+    // stage. That means all messages should be on the server now, so test that.
+    ItemFetchJob *job = new ItemFetchJob(mMaildirCollection);
+    job->fetchScope().fetchFullPayload();
+    Q_ASSERT(job->exec());
+    Item::List items = job->items();
+    Q_ASSERT(mails.size() == items.size());
+
+    QSet<QByteArray> ourMailBodies;
+    QSet<QByteArray> itemMailBodies;
+
+    foreach (const Item &item, items) {
+        KMime::Message::Ptr itemMail = item.payload<KMime::Message::Ptr>();
+        QByteArray itemMailBody = itemMail->body();
+
+        // For some reason, the body in the maildir has one additional newline.
+        // Get rid of this so we can compare them.
+        // FIXME: is this a bug? Find out where the newline comes from!
+        itemMailBody.chop(1);
+        itemMailBodies.insert(itemMailBody);
+    }
+
+    foreach (const QByteArray &mail, mails) {
+        KMime::Message::Ptr ourMail(new KMime::Message());
+        ourMail->setContent(KMime::CRLFtoLF(mail));
+        ourMail->parse();
+        QByteArray ourMailBody = ourMail->body();
+        ourMailBodies.insert(ourMailBody);
+    }
+
+    Q_ASSERT(ourMailBodies == itemMailBodies);
+    return items;
+}
+
+void Pop3Test::syncAndWaitForFinish()
+{
+    AgentManager::self()->instance(mPop3Identifier).synchronize();
+
+    // The pop3 resource, ioslave and the fakeserver are all in different processes or threads.
+    // We simply wait until the FakeServer got disconnected or until a timeout.
+    // Since POP3 fetching can take longer, we reset the timeout timer when the FakeServer
+    // does some processing.
+    QTime time;
+    time.start();
+    int lastProgress = -1;
+    forever {
+        qApp->processEvents();
+
+        // Finish correctly when the connection got closed
+        if (mFakeServerThread->server()->gotDisconnected()) {
+            break;
+        }
+
+        // Reset the timeout when the server is working
+        const int newProgress = mFakeServerThread->server()->progress();
+        if (newProgress != lastProgress) {
+            time.restart();
+            lastProgress = newProgress;
+        }
+
+        // Assert when nothing happens for a certain timeout, that indicates something went
+        // wrong and is stuck somewhere
+        if (time.elapsed() >= 60000) {
+            Q_ASSERT_X(false, "poptest", "FakeServer timed out.");
+            break;
+        }
+    }
+}
+
+QString Pop3Test::loginSequence() const
+{
+    return
+        QStringLiteral("C: USER HansWurst\r\n"
+                       "S: +OK May I have your password, please?\r\n"
+                       "C: PASS Geheim\r\n"
+                       "S: +OK Mailbox locked and ready\r\n");
+}
+
+QString Pop3Test::retrieveSequence(const QList<QByteArray> &mails,
+                                   const QList<int> &exceptions) const
+{
+    QString result;
+    for (int i = 1; i <= mails.size(); i++) {
+        if (!exceptions.contains(i)) {
+            result += QLatin1String(
+                          "C: RETR %RETR%\r\n"
+                          "S: +OK Here is your spam\r\n"
+                          "%MAIL%\r\n"
+                          ".\r\n");
+        }
+    }
+    return result;
+}
+
+QString Pop3Test::deleteSequence(int numToDelete) const
+{
+    QString result;
+    for (int i = 0; i < numToDelete; i++) {
+        result +=
+            QLatin1String("C: DELE %DELE%\r\n"
+                          "S: +OK message sent to /dev/null\r\n");
+    }
+    return result;
+}
+
+QString Pop3Test::quitSequence() const
+{
+    return
+        QStringLiteral("C: QUIT\r\n"
+                       "S: +OK Have a nice day.\r\n");
+}
+
+QString Pop3Test::listSequence(const QList<QByteArray> &mails) const
+{
+    QString result = QStringLiteral("C: LIST\r\n"
+                                    "S: +OK You got new spam\r\n");
+    for (int i = 1; i <= mails.size(); i++) {
+        result += QStringLiteral("%1 %MAILSIZE%\r\n").arg(i);
+    }
+    result += QLatin1String(".\r\n");
+    return result;
+}
+
+QString Pop3Test::uidSequence(const QStringList &uids) const
+{
+    QString result = QStringLiteral("C: UIDL\r\n"
+                                    "S: +OK\r\n");
+    for (int i = 1; i <= uids.size(); i++) {
+        result += QStringLiteral("%1 %2\r\n").arg(i).arg(uids[i - 1]);
+    }
+    result += QLatin1String(".\r\n");
+    return result;
+}
+
+static bool sortedEqual(const QStringList &list1, const QStringList &list2)
+{
+    QStringList sorted1 = list1;
+    sorted1.sort();
+    QStringList sorted2 = list2;
+    sorted2.sort();
+
+    return qEqual(sorted1.begin(), sorted1.end(), sorted2.begin());
+}
+
+void Pop3Test::lowerTimeOfSeenMail(const QString &uidOfMail, int secondsToLower)
+{
+    const int index = mPOP3SettingsInterface->seenUidList().value().indexOf(uidOfMail);
+    QList<int> seenTimeList = mPOP3SettingsInterface->seenUidTimeList().value();
+    int msgTime = seenTimeList.at(index);
+    msgTime -= secondsToLower;
+    seenTimeList.replace(index, msgTime);
+    mPOP3SettingsInterface->setSeenUidTimeList(seenTimeList);
+}
+
+void Pop3Test::testSimpleDownload()
+{
+    QList<QByteArray> mails;
+    mails << simpleMail1 << simpleMail2 << simpleMail3;
+    QStringList uids;
+    uids << QStringLiteral("UID1") << QStringLiteral("UID2") << QStringLiteral("UID3");
+    mFakeServerThread->server()->setAllowedDeletions(QStringLiteral("1,2,3"));
+    mFakeServerThread->server()->setAllowedRetrieves(QStringLiteral("1,2,3"));
+    mFakeServerThread->server()->setMails(mails);
+    mFakeServerThread->server()->setNextConversation(
+        loginSequence() +
+        listSequence(mails) +
+        uidSequence(uids) +
+        retrieveSequence(mails) +
+        deleteSequence(mails.size()) +
+        quitSequence()
+    );
+
+    syncAndWaitForFinish();
+    Akonadi::Item::List items = checkMailsOnAkonadiServer(mails);
+    checkMailsInMaildir(mails);
+    cleanupMaildir(items);
+}
+
+void Pop3Test::testBigFetch()
+{
+    QList<QByteArray> mails;
+    QStringList uids;
+    QString allowedRetrs;
+    for (int i = 0; i < 1000; i++) {
+        QByteArray newMail = simpleMail1;
+        newMail.append(QString::number(i + 1).toLatin1());
+        mails << newMail;
+        uids << QStringLiteral("UID%1").arg(i + 1);
+        allowedRetrs += QString::number(i + 1) + QLatin1Char(',');
+    }
+    allowedRetrs.chop(1);
+
+    mFakeServerThread->server()->setMails(mails);
+    mFakeServerThread->server()->setAllowedRetrieves(allowedRetrs);
+    mFakeServerThread->server()->setAllowedDeletions(allowedRetrs);
+    mFakeServerThread->server()->setNextConversation(
+        loginSequence() +
+        listSequence(mails) +
+        uidSequence(uids) +
+        retrieveSequence(mails) +
+        deleteSequence(mails.size()) +
+        quitSequence()
+    );
+
+    syncAndWaitForFinish();
+    Akonadi::Item::List items = checkMailsOnAkonadiServer(mails);
+    checkMailsInMaildir(mails);
+    cleanupMaildir(items);
+}
+
+void Pop3Test::testSeenUIDCleanup()
+{
+    //
+    // First, fetch 3 normal mails, but leave them on the server.
+    //
+    mPOP3SettingsInterface->setLeaveOnServer(true);
+    QList<QByteArray> mails;
+    mails << simpleMail1 << simpleMail2 << simpleMail3;
+    QStringList uids;
+    uids << QStringLiteral("UID1") << QStringLiteral("UID2") << QStringLiteral("UID3");
+    mFakeServerThread->server()->setAllowedDeletions(QString());
+    mFakeServerThread->server()->setAllowedRetrieves(QStringLiteral("1,2,3"));
+    mFakeServerThread->server()->setMails(mails);
+    mFakeServerThread->server()->setNextConversation(
+        loginSequence() +
+        listSequence(mails) +
+        uidSequence(uids) +
+        retrieveSequence(mails) +
+        quitSequence()
+    );
+
+    syncAndWaitForFinish();
+    Akonadi::Item::List items = checkMailsOnAkonadiServer(mails);
+    checkMailsInMaildir(mails);
+    cleanupMaildir(items);
+
+    QVERIFY(sortedEqual(uids, mPOP3SettingsInterface->seenUidList().value()));
+    QVERIFY(mPOP3SettingsInterface->seenUidTimeList().value().size() ==
+            mPOP3SettingsInterface->seenUidList().value().size());
+
+    //
+    // Now, pretend that the messages were removed from the server in the meantime
+    // by having no mails on the fake server.
+    //
+    mFakeServerThread->server()->setMails(QList<QByteArray>());
+    mFakeServerThread->server()->setAllowedRetrieves(QString());
+    mFakeServerThread->server()->setAllowedDeletions(QString());
+    mFakeServerThread->server()->setNextConversation(
+        loginSequence() +
+        listSequence(QList<QByteArray>()) +
+        uidSequence(QStringList()) +
+        quitSequence()
+    );
+    syncAndWaitForFinish();
+    items = checkMailsOnAkonadiServer(QList<QByteArray>());
+    checkMailsInMaildir(QList<QByteArray>());
+    cleanupMaildir(items);
+
+    QVERIFY(mPOP3SettingsInterface->seenUidList().value().isEmpty());
+    QVERIFY(mPOP3SettingsInterface->seenUidTimeList().value().size() ==
+            mPOP3SettingsInterface->seenUidList().value().size());
+
+    mPOP3SettingsInterface->setLeaveOnServer(false);
+}
+
+void Pop3Test::testSimpleLeaveOnServer()
+{
+    mPOP3SettingsInterface->setLeaveOnServer(true);
+
+    QList<QByteArray> mails;
+    mails << simpleMail1 << simpleMail2 << simpleMail3;
+    QStringList uids;
+    uids << QStringLiteral("UID1") << QStringLiteral("UID2") << QStringLiteral("UID3");
+    mFakeServerThread->server()->setMails(mails);
+    mFakeServerThread->server()->setAllowedRetrieves(QStringLiteral("1,2,3"));
+    mFakeServerThread->server()->setNextConversation(
+        loginSequence() +
+        listSequence(mails) +
+        uidSequence(uids) +
+        retrieveSequence(mails) +
+        quitSequence()
+    );
+
+    syncAndWaitForFinish();
+    Akonadi::Item::List items = checkMailsOnAkonadiServer(mails);
+    checkMailsInMaildir(mails);
+
+    // The resource should have saved the UIDs of the seen messages
+    QVERIFY(sortedEqual(uids, mPOP3SettingsInterface->seenUidList().value()));
+    QVERIFY(mPOP3SettingsInterface->seenUidTimeList().value().size() ==
+            mPOP3SettingsInterface->seenUidList().value().size());
+    foreach (int seenTime, mPOP3SettingsInterface->seenUidTimeList().value()) {
+        // Those message were just downloaded from the fake server, so they are at maximum
+        // 10 minutes old (for slooooow running tests)
+        QVERIFY(seenTime >= time(Q_NULLPTR) - 10 * 60);
+    }
+
+    //
+    // OK, next mail check: We have to check that the old seen messages are not downloaded again,
+    // only new mails.
+    //
+    QList<QByteArray> newMails(mails);
+    newMails << simpleMail4;
+    QStringList newUids(uids);
+    newUids << QStringLiteral("newUID");
+    QList<int> idsToNotDownload;
+    idsToNotDownload << 1 << 2 << 3;
+    mFakeServerThread->server()->setMails(newMails);
+    mFakeServerThread->server()->setAllowedRetrieves(QStringLiteral("4"));
+    mFakeServerThread->server()->setNextConversation(
+        loginSequence() +
+        listSequence(newMails) +
+        uidSequence(newUids) +
+        retrieveSequence(newMails, idsToNotDownload) +
+        quitSequence(),
+        idsToNotDownload
+    );
+
+    syncAndWaitForFinish();
+    items = checkMailsOnAkonadiServer(newMails);
+    checkMailsInMaildir(newMails);
+    QVERIFY(sortedEqual(newUids, mPOP3SettingsInterface->seenUidList().value()));
+    QVERIFY(mPOP3SettingsInterface->seenUidTimeList().value().size() ==
+            mPOP3SettingsInterface->seenUidList().value().size());
+
+    //
+    // Ok, next test: When turning off leaving on the server, all mails should be deleted, but
+    // none downloaded.
+    //
+    mPOP3SettingsInterface->setLeaveOnServer(false);
+
+    mFakeServerThread->server()->setAllowedDeletions(QStringLiteral("1,2,3,4"));
+    mFakeServerThread->server()->setAllowedRetrieves(QString());
+    mFakeServerThread->server()->setNextConversation(
+        loginSequence() +
+        listSequence(newMails) +
+        uidSequence(newUids) +
+        deleteSequence(newMails.size()) +
+        quitSequence()
+    );
+
+    syncAndWaitForFinish();
+    items = checkMailsOnAkonadiServer(newMails);
+    checkMailsInMaildir(newMails);
+    cleanupMaildir(items);
+    QVERIFY(mPOP3SettingsInterface->seenUidList().value().isEmpty());
+    QVERIFY(mPOP3SettingsInterface->seenUidTimeList().value().size() ==
+            mPOP3SettingsInterface->seenUidList().value().size());
+}
+
+void Pop3Test::testTimeBasedLeaveRule()
+{
+    mPOP3SettingsInterface->setLeaveOnServer(true);
+    mPOP3SettingsInterface->setLeaveOnServerDays(2);
+
+    //
+    // First download 3 mails and leave them on the server
+    //
+    QList<QByteArray> mails;
+    mails << simpleMail1 << simpleMail2 << simpleMail3;
+    QStringList uids;
+    uids << QStringLiteral("UID1") << QStringLiteral("UID2") << QStringLiteral("UID3");
+    mFakeServerThread->server()->setMails(mails);
+    mFakeServerThread->server()->setAllowedRetrieves(QStringLiteral("1,2,3"));
+    mFakeServerThread->server()->setNextConversation(
+        loginSequence() +
+        listSequence(mails) +
+        uidSequence(uids) +
+        retrieveSequence(mails) +
+        quitSequence()
+    );
+
+    syncAndWaitForFinish();
+    Akonadi::Item::List items = checkMailsOnAkonadiServer(mails);
+    checkMailsInMaildir(mails);
+
+    QVERIFY(sortedEqual(uids, mPOP3SettingsInterface->seenUidList().value()));
+    QVERIFY(mPOP3SettingsInterface->seenUidTimeList().value().size() ==
+            mPOP3SettingsInterface->seenUidList().value().size());
+
+    //
+    // Now, modify the seenUidTimeList on the server for UID2 to pretend it
+    // was downloaded 3 days ago, which means it should be deleted.
+    //
+    lowerTimeOfSeenMail(QStringLiteral("UID2"), 60 * 60 * 24 * 3);
+
+    QList<int> idsToNotDownload;
+    idsToNotDownload << 1 << 3;
+    mFakeServerThread->server()->setAllowedDeletions(QStringLiteral("2"));
+    mFakeServerThread->server()->setAllowedRetrieves(QString());
+    mFakeServerThread->server()->setNextConversation(
+        loginSequence() +
+        listSequence(mails) +
+        uidSequence(uids) +
+        deleteSequence(1) +
+        quitSequence(),
+        idsToNotDownload
+    );
+    syncAndWaitForFinish();
+    items = checkMailsOnAkonadiServer(mails);
+    checkMailsInMaildir(mails);
+    cleanupMaildir(items);
+
+    uids.removeAll(QStringLiteral("UID2"));
+    QVERIFY(sortedEqual(uids, mPOP3SettingsInterface->seenUidList().value()));
+    QVERIFY(mPOP3SettingsInterface->seenUidTimeList().value().size() ==
+            mPOP3SettingsInterface->seenUidList().value().size());
+    foreach (int seenTime, mPOP3SettingsInterface->seenUidTimeList().value()) {
+        QVERIFY(seenTime >= time(Q_NULLPTR) - 10 * 60);
+    }
+
+    mPOP3SettingsInterface->setLeaveOnServer(false);
+    mPOP3SettingsInterface->setLeaveOnServerDays(0);
+    mPOP3SettingsInterface->setSeenUidTimeList(QList<int>());
+    mPOP3SettingsInterface->setSeenUidList(QStringList());
+}
+
+void Pop3Test::testCountBasedLeaveRule()
+{
+    mPOP3SettingsInterface->setLeaveOnServer(true);
+    mPOP3SettingsInterface->setLeaveOnServerCount(3);
+
+    //
+    // First download 3 mails and leave them on the server
+    //
+    QList<QByteArray> mails;
+    mails << simpleMail1 << simpleMail2 << simpleMail3;
+    QStringList uids;
+    uids << QStringLiteral("UID1") << QStringLiteral("UID2") << QStringLiteral("UID3");
+    mFakeServerThread->server()->setMails(mails);
+    mFakeServerThread->server()->setAllowedRetrieves(QStringLiteral("1,2,3"));
+    mFakeServerThread->server()->setNextConversation(
+        loginSequence() +
+        listSequence(mails) +
+        uidSequence(uids) +
+        retrieveSequence(mails) +
+        quitSequence()
+    );
+
+    syncAndWaitForFinish();
+    checkMailsOnAkonadiServer(mails);
+    checkMailsInMaildir(mails);
+
+    // Make the 3 just downloaded mails appear older than they are
+    lowerTimeOfSeenMail(QStringLiteral("UID1"), 60 * 60 * 24 * 2);
+    lowerTimeOfSeenMail(QStringLiteral("UID2"), 60 * 60 * 24 * 1);
+    lowerTimeOfSeenMail(QStringLiteral("UID3"), 60 * 60 * 24 * 3);
+
+    //
+    // Now, download 2 more mails. Since only 3 mails are allowed to be left
+    // on the server, the oldest ones, UID1 and UID3, should be deleted
+    //
+    QList<QByteArray> moreMails;
+    moreMails << simpleMail4 << simpleMail5;
+    QStringList moreUids;
+    moreUids << QStringLiteral("UID4") << QStringLiteral("UID5");
+    mFakeServerThread->server()->setMails(mails + moreMails);
+    mFakeServerThread->server()->setAllowedRetrieves(QStringLiteral("4,5"));
+    mFakeServerThread->server()->setAllowedDeletions(QStringLiteral("1,3"));
+    mFakeServerThread->server()->setNextConversation(
+        loginSequence() +
+        listSequence(mails + moreMails) +
+        uidSequence(uids + moreUids) +
+        retrieveSequence(moreMails) +
+        deleteSequence(2) +
+        quitSequence(), QList<int>() << 1 << 2 << 3
+    );
+
+    syncAndWaitForFinish();
+    Akonadi::Item::List items = checkMailsOnAkonadiServer(mails + moreMails);
+    checkMailsInMaildir(mails + moreMails);
+    cleanupMaildir(items);
+
+    QStringList uidsLeft;
+    uidsLeft << QStringLiteral("UID2") << QStringLiteral("UID4") << QStringLiteral("UID5");
+
+    QVERIFY(sortedEqual(uidsLeft, mPOP3SettingsInterface->seenUidList().value()));
+    QVERIFY(mPOP3SettingsInterface->seenUidTimeList().value().size() ==
+            mPOP3SettingsInterface->seenUidList().value().size());
+
+    mPOP3SettingsInterface->setLeaveOnServer(false);
+    mPOP3SettingsInterface->setLeaveOnServerCount(0);
+    mPOP3SettingsInterface->setSeenUidTimeList(QList<int>());
+    mPOP3SettingsInterface->setSeenUidList(QStringList());
+}
+
+void Pop3Test::testSizeBasedLeaveRule()
+{
+    mPOP3SettingsInterface->setLeaveOnServer(true);
+    mPOP3SettingsInterface->setLeaveOnServerSize(10);   // 10 MB
+
+    //
+    // First download 3 mails and leave them on the server.
+    //
+    QList<QByteArray> mails;
+    mails << simpleMail1 << simpleMail2 << simpleMail3;
+    QStringList uids;
+    uids << QStringLiteral("UID1") << QStringLiteral("UID2") << QStringLiteral("UID3");
+    mFakeServerThread->server()->setMails(mails);
+    mFakeServerThread->server()->setAllowedRetrieves(QStringLiteral("1,2,3"));
+    mFakeServerThread->server()->setNextConversation(
+        loginSequence() +
+        listSequence(mails) +
+        uidSequence(uids) +
+        retrieveSequence(mails) +
+        quitSequence()
+    );
+
+    syncAndWaitForFinish();
+    checkMailsOnAkonadiServer(mails);
+    checkMailsInMaildir(mails);
+
+    // Make the 3 just downloaded mails appear older than they are
+    lowerTimeOfSeenMail(QStringLiteral("UID1"), 60 * 60 * 24 * 2);
+    lowerTimeOfSeenMail(QStringLiteral("UID2"), 60 * 60 * 24 * 1);
+    lowerTimeOfSeenMail(QStringLiteral("UID3"), 60 * 60 * 24 * 3);
+
+    // Now, do another mail check, but with no new mails on the server.
+    // Instead we let the server pretend that the mails have a fake size,
+    // each 7 MB. That means the two oldest get deleted, because the total
+    // mail size is over 10 MB with them.
+    mFakeServerThread->server()->setMails(mails);
+    mFakeServerThread->server()->setAllowedRetrieves(QString());
+    mFakeServerThread->server()->setAllowedDeletions(QStringLiteral("1,3"));
+    mFakeServerThread->server()->setNextConversation(
+        loginSequence() +
+        QLatin1String("C: LIST\r\n"
+                      "S: +OK You got new spam\r\n"
+                      "1 7340032\r\n"
+                      "2 7340032\r\n"
+                      "3 7340032\r\n"
+                      ".\r\n") +
+        uidSequence(uids) +
+        deleteSequence(2) +
+        quitSequence()
+    );
+
+    syncAndWaitForFinish();
+    Akonadi::Item::List items = checkMailsOnAkonadiServer(mails);
+    checkMailsInMaildir(mails);
+    cleanupMaildir(items);
+
+    QStringList uidsLeft;
+    uidsLeft << QStringLiteral("UID2");
+
+    QVERIFY(sortedEqual(uidsLeft, mPOP3SettingsInterface->seenUidList().value()));
+    QVERIFY(mPOP3SettingsInterface->seenUidTimeList().value().size() ==
+            mPOP3SettingsInterface->seenUidList().value().size());
+
+    mPOP3SettingsInterface->setLeaveOnServer(false);
+    mPOP3SettingsInterface->setLeaveOnServerCount(0);
+    mPOP3SettingsInterface->setLeaveOnServerSize(0);
+    mPOP3SettingsInterface->setSeenUidTimeList(QList<int>());
+    mPOP3SettingsInterface->setSeenUidList(QStringList());
+}
+
+void Pop3Test::testMixedLeaveRules()
+{
+    mPOP3SettingsInterface->setLeaveOnServer(true);
+    //
+    // Generate 10 mails
+    //
+    QList<QByteArray> mails;
+    QStringList uids;
+    QString allowedRetrs;
+    for (int i = 0; i < 10; i++) {
+        QByteArray newMail = simpleMail1;
+        newMail.append(QString::number(i + 1).toLatin1());
+        mails << newMail;
+        uids << QStringLiteral("UID%1").arg(i + 1);
+        allowedRetrs += QString::number(i + 1) + QLatin1Char(',');
+    }
+    allowedRetrs.chop(1);
+
+    //
+    // Now, download these 10 mails
+    //
+    mFakeServerThread->server()->setMails(mails);
+    mFakeServerThread->server()->setAllowedRetrieves(allowedRetrs);
+    mFakeServerThread->server()->setNextConversation(
+        loginSequence() +
+        listSequence(mails) +
+        uidSequence(uids) +
+        retrieveSequence(mails) +
+        quitSequence()
+    );
+
+    syncAndWaitForFinish();
+    checkMailsOnAkonadiServer(mails);
+    checkMailsInMaildir(mails);
+
+    // Fake the time of the messages, UID1 is one day old, UID2 is two days old, etc
+    for (int i = 1; i <= 10; i++) {
+        lowerTimeOfSeenMail(QStringLiteral("UID%1").arg(i), 60 * 60 * 24 * i);
+    }
+
+    mPOP3SettingsInterface->setLeaveOnServer(true);
+    mPOP3SettingsInterface->setLeaveOnServerSize(25);   // UID 4, 5 oldest here
+    mPOP3SettingsInterface->setLeaveOnServerCount(5);   // UID 6, 7 oldest here
+    mPOP3SettingsInterface->setLeaveOnServerDays(7);    // UID 8, 9 and 10 too old
+
+    // Ok, now we do another mail check that only deletes stuff from the server.
+    // Above are the UIDs that should be deleted.
+    mFakeServerThread->server()->setMails(mails);
+    mFakeServerThread->server()->setAllowedRetrieves(QString());
+    mFakeServerThread->server()->setAllowedDeletions(QStringLiteral("4,5,6,7,8,9,10"));
+    mFakeServerThread->server()->setNextConversation(
+        loginSequence() +
+        QLatin1String("C: LIST\r\n"
+                      "S: +OK You got new spam\r\n"
+                      "1 7340032\r\n"
+                      "2 7340032\r\n"
+                      "3 7340032\r\n"
+                      "4 7340032\r\n"
+                      "5 7340032\r\n"
+                      "6 7340032\r\n"
+                      "7 7340032\r\n"
+                      "8 7340032\r\n"
+                      "9 7340032\r\n"
+                      "10 7340032\r\n"
+                      ".\r\n") +
+        uidSequence(uids) +
+        deleteSequence(7) +
+        quitSequence()
+    );
+
+    syncAndWaitForFinish();
+    Akonadi::Item::List items = checkMailsOnAkonadiServer(mails);
+    checkMailsInMaildir(mails);
+    cleanupMaildir(items);
+
+    QStringList uidsLeft;
+    uidsLeft << QStringLiteral("UID1") << QStringLiteral("UID2") << QStringLiteral("UID3");
+
+    QVERIFY(sortedEqual(uidsLeft, mPOP3SettingsInterface->seenUidList().value()));
+    QVERIFY(mPOP3SettingsInterface->seenUidTimeList().value().size() ==
+            mPOP3SettingsInterface->seenUidList().value().size());
+
+    mPOP3SettingsInterface->setLeaveOnServer(false);
+    mPOP3SettingsInterface->setLeaveOnServerCount(0);
+    mPOP3SettingsInterface->setLeaveOnServerSize(0);
+    mPOP3SettingsInterface->setSeenUidTimeList(QList<int>());
+    mPOP3SettingsInterface->setSeenUidList(QStringList());
+}
diff --git a/resources/pop3/autotests/pop3test.h b/resources/pop3/autotests/pop3test.h
new file mode 100644 (file)
index 0000000..981c4e7
--- /dev/null
@@ -0,0 +1,73 @@
+/* Copyright 2009 Thomas McGuire <mcguire@kde.org>
+
+   This library is free software; you can redistribute it and/or modify
+   it under the terms of the GNU Library General Public License as published
+   by the Free Software Foundation; either version 2 of the License or
+   ( at your option ) version 3 or, at the discretion of KDE e.V.
+   ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version.
+
+   This library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Library General Public License for more details.
+
+   You should have received a copy of the GNU Library General Public License
+   along with this library; see the file COPYING.LIB.  If not, write to
+   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+   Boston, MA 02110-1301, USA.
+*/
+#ifndef POP3TEST_H
+#define POP3TEST_H
+
+#include "fakeserver/fakeserver.h"
+#include "pop3settings.h"
+#include "maildirsettings.h"
+
+#include <AkonadiCore/Collection>
+#include <AkonadiCore/Item>
+
+#include <QtCore/QObject>
+#include <QtCore/QList>
+
+class Pop3Test : public QObject
+{
+    Q_OBJECT
+
+    void replymMaildirSettingsInterface(QString arg1);
+private Q_SLOTS:
+    void initTestCase();
+    void cleanupTestCase();
+    void testSimpleDownload();
+    void testSimpleLeaveOnServer();
+    void testBigFetch();
+    void testSeenUIDCleanup();
+    void testTimeBasedLeaveRule();
+    void testCountBasedLeaveRule();
+    void testSizeBasedLeaveRule();
+    void testMixedLeaveRules();
+
+private:
+    void lowerTimeOfSeenMail(const QString &uidOfMail, int secondsToLower);
+    void cleanupMaildir(Akonadi::Item::List items);
+    void checkMailsInMaildir(const QList< QByteArray > &mails);
+    Akonadi::Item::List checkMailsOnAkonadiServer(const QList<QByteArray> &mails);
+    void syncAndWaitForFinish();
+    QString loginSequence() const;
+    QString retrieveSequence(const QList< QByteArray > &mails,
+                             const QList<int> &exceptions = QList<int>()) const;
+    QString deleteSequence(int numToDelete) const;
+    QString quitSequence() const;
+    QString listSequence(const QList<QByteArray> &mails) const;
+    QString uidSequence(const QStringList &uids) const;
+
+    FakeServerThread *mFakeServerThread;
+
+    OrgKdeAkonadiPOP3SettingsInterface *mPOP3SettingsInterface;
+    OrgKdeAkonadiMaildirSettingsInterface *mMaildirSettingsInterface;
+    Akonadi::Collection mMaildirCollection;
+    QString mPop3Identifier;
+    QString mMaildirIdentifier;
+    QString mMaildirPath;
+};
+
+#endif
diff --git a/resources/pop3/autotests/unittestenv/config.xml b/resources/pop3/autotests/unittestenv/config.xml
new file mode 100644 (file)
index 0000000..fa0eafe
--- /dev/null
@@ -0,0 +1,6 @@
+<config>
+  <kdehome>kdehome</kdehome>
+  <confighome>xdgconfig</confighome>
+  <datahome>xdglocal</datahome>
+  <envvar name="AKONADI_DISABLE_AGENT_AUTOSTART">true</envvar>
+</config>
diff --git a/resources/pop3/autotests/unittestenv/kdehome/share/apps/.keep b/resources/pop3/autotests/unittestenv/kdehome/share/apps/.keep
new file mode 100644 (file)
index 0000000..2f1d07a
--- /dev/null
@@ -0,0 +1 @@
+force git to include this file
diff --git a/resources/pop3/autotests/unittestenv/kdehome/share/config/akonadi-firstrunrc b/resources/pop3/autotests/unittestenv/kdehome/share/config/akonadi-firstrunrc
new file mode 100644 (file)
index 0000000..1cac492
--- /dev/null
@@ -0,0 +1,3 @@
+[ProcessedDefaults]
+defaultaddressbook=done
+defaultcalendar=done
diff --git a/resources/pop3/autotests/unittestenv/kdehome/share/config/kdebugrc b/resources/pop3/autotests/unittestenv/kdehome/share/config/kdebugrc
new file mode 100755 (executable)
index 0000000..3818116
--- /dev/null
@@ -0,0 +1,103 @@
+[0]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
+
+[kbuildsycoca4]
+AbortFatal=true
+ErrorOutput=0
+FatalOutput=0
+InfoOutput=0
+WarnOutput=0
+
+[kdecore (services)]
+AbortFatal=true
+ErrorOutput=0
+FatalOutput=0
+InfoOutput=0
+WarnOutput=0
+
+[264]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=4
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=4
+InfoFilename[$e]=kdebug.dbg
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=4
+
+[5250]
+InfoOutput=2
+
+[7009]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=4
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=4
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=4
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=4
+
+[7011]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=4
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=4
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=4
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=4
+
+[7012]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=4
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=4
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=4
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=4
+
+[7014]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=0
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=0
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=0
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=0
+
+[7021]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=4
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=4
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=4
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=4
+
+[7105]
+AbortFatal=true
+ErrorFilename[$e]=kdebug.dbg
+ErrorOutput=2
+FatalFilename[$e]=kdebug.dbg
+FatalOutput=2
+InfoFilename[$e]=kdebug.dbg
+InfoOutput=2
+WarnFilename[$e]=kdebug.dbg
+WarnOutput=2
diff --git a/resources/pop3/autotests/unittestenv/xdgconfig/akonadi/akonadiserverrc b/resources/pop3/autotests/unittestenv/xdgconfig/akonadi/akonadiserverrc
new file mode 100644 (file)
index 0000000..7f738ce
--- /dev/null
@@ -0,0 +1,4 @@
+[%General]
+
+[Search]
+Manager=Dummy
diff --git a/resources/pop3/autotests/unittestenv/xdglocal/.keep b/resources/pop3/autotests/unittestenv/xdglocal/.keep
new file mode 100644 (file)
index 0000000..2f1d07a
--- /dev/null
@@ -0,0 +1 @@
+force git to include this file
diff --git a/resources/pop3/jobs.cpp b/resources/pop3/jobs.cpp
new file mode 100644 (file)
index 0000000..23443d8
--- /dev/null
@@ -0,0 +1,500 @@
+/* Copyright 2009 Thomas McGuire <mcguire@kde.org>
+
+   This library is free software; you can redistribute it and/or modify
+   it under the terms of the GNU Library General Public License as published
+   by the Free Software Foundation; either version 2 of the License or
+   ( at your option ) version 3 or, at the discretion of KDE e.V.
+   ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version.
+
+   This library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Library General Public License for more details.
+
+   You should have received a copy of the GNU Library General Public License
+   along with this library; see the file COPYING.LIB.  If not, write to
+   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+   Boston, MA 02110-1301, USA.
+*/
+
+#include "jobs.h"
+#include "settings.h"
+
+#include <MailTransport/Transport>
+
+#include <KIO/Scheduler>
+#include <KIO/Slave>
+#include <KIO/Job>
+#include <KIO/TransferJob>
+#include "pop3resource_debug.h"
+#include <KLocalizedString>
+
+POPSession::POPSession(const QString &password)
+    : mCurrentJob(Q_NULLPTR), mPassword(password)
+{
+    KIO::Scheduler::connect(SIGNAL(slaveError(KIO::Slave*,int,QString)), this, SLOT(slotSlaveError(KIO::Slave*,int,QString)));
+}
+
+POPSession::~POPSession()
+{
+    closeSession();
+}
+
+void POPSession::slotSlaveError(KIO::Slave *slave, int errorCode,
+                                const QString &errorMessage)
+{
+    Q_UNUSED(slave);
+    qCWarning(POP3RESOURCE_LOG) << "Got a slave error:" << errorMessage;
+
+    if (slave != mSlave) {
+        return;
+    }
+
+    if (errorCode == KIO::ERR_SLAVE_DIED) {
+        mSlave = Q_NULLPTR;
+    }
+
+    // Explicitly disconnect the slave if the connection went down
+    if (errorCode == KIO::ERR_CONNECTION_BROKEN && mSlave) {
+        KIO::Scheduler::disconnectSlave(mSlave);
+        mSlave = Q_NULLPTR;
+    }
+
+    if (!mCurrentJob) {
+        Q_EMIT slaveError(errorCode, errorMessage);
+    } else {
+        // Let the job deal with the problem
+        mCurrentJob->slaveError(errorCode, errorMessage);
+    }
+}
+
+void POPSession::setCurrentJob(SlaveBaseJob *job)
+{
+    mCurrentJob = job;
+}
+
+KIO::MetaData POPSession::slaveConfig() const
+{
+    KIO::MetaData m;
+
+    m.insert(QStringLiteral("progress"), QStringLiteral("off"));
+    m.insert(QStringLiteral("tls"), Settings::self()->useTLS() ? QStringLiteral("on") : QStringLiteral("off"));
+    m.insert(QStringLiteral("pipelining"), (Settings::self()->pipelining()) ? QStringLiteral("on") : QStringLiteral("off"));
+    int type = Settings::self()->authenticationMethod();
+    switch (type) {
+    case MailTransport::Transport::EnumAuthenticationType::PLAIN:
+    case MailTransport::Transport::EnumAuthenticationType::LOGIN:
+    case MailTransport::Transport::EnumAuthenticationType::CRAM_MD5:
+    case MailTransport::Transport::EnumAuthenticationType::DIGEST_MD5:
+    case MailTransport::Transport::EnumAuthenticationType::NTLM:
+    case MailTransport::Transport::EnumAuthenticationType::GSSAPI:
+        m.insert(QStringLiteral("auth"), QStringLiteral("SASL"));
+        m.insert(QStringLiteral("sasl"), authenticationToString(type));
+        break;
+    case MailTransport::Transport::EnumAuthenticationType::CLEAR:
+        m.insert(QStringLiteral("auth"), QStringLiteral("USER"));
+        break;
+    default:
+        m.insert(QStringLiteral("auth"), authenticationToString(type));
+        break;
+    }
+    return m;
+}
+
+QString POPSession::authenticationToString(int type) const
+{
+    switch (type) {
+    case MailTransport::Transport::EnumAuthenticationType::LOGIN:
+        return QStringLiteral("LOGIN");
+    case MailTransport::Transport::EnumAuthenticationType::PLAIN:
+        return QStringLiteral("PLAIN");
+    case MailTransport::Transport::EnumAuthenticationType::CRAM_MD5:
+        return QStringLiteral("CRAM-MD5");
+    case MailTransport::Transport::EnumAuthenticationType::DIGEST_MD5:
+        return QStringLiteral("DIGEST-MD5");
+    case MailTransport::Transport::EnumAuthenticationType::GSSAPI:
+        return QStringLiteral("GSSAPI");
+    case MailTransport::Transport::EnumAuthenticationType::NTLM:
+        return QStringLiteral("NTLM");
+    case  MailTransport::Transport::EnumAuthenticationType::CLEAR:
+        return QStringLiteral("USER");
+    case MailTransport::Transport::EnumAuthenticationType::APOP:
+        return QStringLiteral("APOP");
+    default:
+        break;
+    }
+    return QString();
+}
+
+QUrl POPSession::getUrl() const
+{
+    QUrl url;
+
+    if (Settings::self()->useSSL()) {
+        url.setScheme(QStringLiteral("pop3s"));
+    } else {
+        url.setScheme(QStringLiteral("pop3"));
+    }
+
+    url.setUserName(Settings::self()->login());
+    url.setPassword(mPassword);
+    url.setHost(Settings::self()->host());
+    url.setPort(Settings::self()->port());
+    return url;
+}
+
+bool POPSession::connectSlave()
+{
+    mSlave = KIO::Scheduler::getConnectedSlave(getUrl(), slaveConfig());
+    return mSlave != Q_NULLPTR;
+}
+
+void POPSession::abortCurrentJob()
+{
+    if (mCurrentJob) {
+        mCurrentJob->kill(KJob::Quietly);
+        mCurrentJob = Q_NULLPTR;
+    }
+}
+
+void POPSession::closeSession()
+{
+    if (mSlave) {
+        KIO::Scheduler::disconnectSlave(mSlave);
+    }
+}
+
+KIO::Slave *POPSession::getSlave() const
+{
+    return mSlave;
+}
+
+static QByteArray cleanupListRespone(const QByteArray &response)
+{
+    QByteArray ret = response.simplified(); // Workaround for Maillennium POP3/UNIBOX
+
+    // Get rid of the null terminating character, if it exists
+    if (ret.size() > 0 && ret.at(ret.size() - 1) == 0) {
+        ret.chop(1);
+    }
+    return ret;
+}
+
+static QString intListToString(const QList<int> &intList)
+{
+    QString idList;
+    foreach (int id, intList) {
+        idList += QString::number(id) + QLatin1Char(',');
+    }
+    idList.chop(1);
+    return idList;
+}
+
+SlaveBaseJob::SlaveBaseJob(POPSession *POPSession)
+    : mJob(Q_NULLPTR),
+      mPOPSession(POPSession)
+{
+    mPOPSession->setCurrentJob(this);
+}
+
+SlaveBaseJob::~SlaveBaseJob()
+{
+    // Don't do that here, the job might be destroyed after another one was started
+    // and therefore overwrite the current job
+    //mPOPSession->setCurrentJob( 0 );
+}
+
+bool SlaveBaseJob::doKill()
+{
+    if (mJob) {
+        return mJob->kill();
+    } else {
+        return KJob::doKill();
+    }
+}
+
+void SlaveBaseJob::slotSlaveResult(KJob *job)
+{
+    mPOPSession->setCurrentJob(Q_NULLPTR);
+    if (job->error()) {
+        setError(job->error());
+        setErrorText(job->errorText());
+    }
+    emitResult();
+    mJob = Q_NULLPTR;
+}
+
+void SlaveBaseJob::slotSlaveData(KIO::Job *job, const QByteArray &data)
+{
+    Q_UNUSED(job);
+    qCWarning(POP3RESOURCE_LOG) << "Got unexpected slave data:" << data.data();
+}
+
+void SlaveBaseJob::slaveError(int errorCode, const QString &errorMessage)
+{
+    // The slave experienced some problem while running our job.
+    // Just treat this as an error.
+    // Derived jobs can do something more sophisticated here
+    setError(errorCode);
+    setErrorText(errorMessage);
+    emitResult();
+    mJob = Q_NULLPTR;
+}
+
+void SlaveBaseJob::connectJob()
+{
+    connect(mJob, &KIO::TransferJob::data, this, &SlaveBaseJob::slotSlaveData);
+    connect(mJob, &KIO::TransferJob::result, this, &SlaveBaseJob::slotSlaveResult);
+}
+
+void SlaveBaseJob::startJob(const QString &path)
+{
+    QUrl url = mPOPSession->getUrl();
+    url.setPath(path);
+    mJob = KIO::get(url, KIO::NoReload, KIO::HideProgressInfo);
+    KIO::Scheduler::assignJobToSlave(mPOPSession->getSlave(), mJob);
+    connectJob();
+}
+
+QString SlaveBaseJob::errorString() const
+{
+    if (mJob) {
+        return mJob->errorString();
+    } else {
+        return KJob::errorString();
+    }
+}
+
+LoginJob::LoginJob(POPSession *popSession)
+    : SlaveBaseJob(popSession)
+{
+}
+
+void LoginJob::start()
+{
+    // This will create a connected slave, which means it will also try to login.
+    KIO::Scheduler::connect(SIGNAL(slaveConnected(KIO::Slave*)), this, SLOT(slaveConnected(KIO::Slave*)));
+    if (!mPOPSession->connectSlave()) {
+        setError(KJob::UserDefinedError);
+        setErrorText(i18n("Unable to create POP3 slave, aborting mail check."));
+        emitResult();
+    }
+}
+
+void LoginJob::slaveConnected(KIO::Slave *slave)
+{
+    if (slave != mPOPSession->getSlave()) {
+        // Odd, not our slave...
+        return;
+    }
+
+    // Yeah it connected, so login was sucessful!
+    emitResult();
+}
+
+void LoginJob::slaveError(int errorCode, const QString &errorMessage)
+{
+    setError(errorCode);
+    setErrorText(errorMessage);
+    mErrorString = KIO::buildErrorString(errorCode, errorMessage);
+    emitResult();
+}
+
+QString LoginJob::errorString() const
+{
+    return mErrorString;
+}
+
+ListJob::ListJob(POPSession *popSession)
+    : SlaveBaseJob(popSession)
+{
+}
+
+void ListJob::start()
+{
+    startJob(QStringLiteral("/index"));
+}
+
+void ListJob::slotSlaveData(KIO::Job *job, const QByteArray &data)
+{
+    Q_UNUSED(job);
+
+    // Silly slave, why are you sending us empty data?
+    if (data.isEmpty()) {
+        return;
+    }
+
+    QByteArray cleanData = cleanupListRespone(data);
+    const int space = cleanData.indexOf(' ');
+
+    if (space > 0) {
+        QByteArray lengthString = cleanData.mid(space + 1);
+        const int spaceInLengthPos = lengthString.indexOf(' ');
+        if (spaceInLengthPos != -1) {
+            lengthString.truncate(spaceInLengthPos);
+        }
+        const int length = lengthString.toInt();
+
+        QByteArray idString = cleanData.left(space);
+
+        bool idIsNumber;
+        int id = QString::fromLatin1(idString).toInt(&idIsNumber);
+        if (idIsNumber) {
+            mIdList.insert(id, length);
+        } else
+            qCWarning(POP3RESOURCE_LOG) << "Got non-integer ID as part of the LIST response, ignoring"
+                                        << idString.data();
+    } else {
+        qCWarning(POP3RESOURCE_LOG) << "Got invalid LIST response:" << data.data();
+    }
+}
+
+QMap<int, int> ListJob::idList() const
+{
+    return mIdList;
+}
+
+UIDListJob::UIDListJob(POPSession *popSession)
+    : SlaveBaseJob(popSession)
+{
+}
+
+void UIDListJob::start()
+{
+    startJob(QStringLiteral("/uidl"));
+}
+
+void UIDListJob::slotSlaveData(KIO::Job *job, const QByteArray &data)
+{
+    Q_UNUSED(job);
+
+    // Silly slave, why are you sending us empty data?
+    if (data.isEmpty()) {
+        return;
+    }
+
+    QByteArray cleanData = cleanupListRespone(data);
+    const int space = cleanData.indexOf(' ');
+
+    if (space <= 0) {
+        qCWarning(POP3RESOURCE_LOG) << "Invalid response to the UIDL command:" << data.data();
+        qCWarning(POP3RESOURCE_LOG) << "Ignoring this entry.";
+    } else {
+        QByteArray idString = cleanData.left(space);
+        QByteArray uidString = cleanData.mid(space + 1);
+        bool idIsNumber;
+        int id = QString::fromLatin1(idString).toInt(&idIsNumber);
+        if (idIsNumber) {
+            const QString uidQString = QString::fromLatin1(uidString);
+            if (!uidQString.isEmpty()) {
+                mUidList.insert(id, uidQString);
+                mIdList.insert(uidQString, id);
+            } else {
+                qCWarning(POP3RESOURCE_LOG) << "Got invalid/empty UID from the UIDL command:"
+                                            << uidString.data();
+                qCWarning(POP3RESOURCE_LOG) << "The whole response was:" << data.data();
+            }
+        } else {
+            qCWarning(POP3RESOURCE_LOG) << "Got invalid ID from the UIDL command:" << idString.data();
+            qCWarning(POP3RESOURCE_LOG) << "The whole response was:" << data.data();
+        }
+    }
+}
+
+QMap<int, QString> UIDListJob::uidList() const
+{
+    return mUidList;
+}
+
+QMap<QString, int> UIDListJob::idList() const
+{
+    return mIdList;
+}
+
+DeleteJob::DeleteJob(POPSession *popSession)
+    : SlaveBaseJob(popSession)
+{
+}
+
+void DeleteJob::setDeleteIds(const QList<int> &ids)
+{
+    mIdsToDelete = ids;
+}
+
+void DeleteJob::start()
+{
+    startJob(QLatin1String("/remove/") + intListToString(mIdsToDelete));
+}
+
+QList<int> DeleteJob::deletedIDs() const
+{
+    // FIXME : The slave doesn't tell us which of the IDs were actually deleted, we
+    //         just assume all of them here
+    return mIdsToDelete;
+}
+
+QuitJob::QuitJob(POPSession *popSession)
+    : SlaveBaseJob(popSession)
+{
+}
+
+void QuitJob::start()
+{
+    startJob(QStringLiteral("/commit"));
+}
+
+FetchJob::FetchJob(POPSession *session)
+    : SlaveBaseJob(session),
+      mBytesDownloaded(0),
+      mTotalBytesToDownload(0),
+      mDataCounter(0)
+{
+}
+
+void FetchJob::setFetchIds(const QList<int> &ids, const QList<int> &sizes)
+{
+    mIdsPendingDownload = ids;
+    foreach (int size, sizes) {
+        mTotalBytesToDownload += size;
+    }
+}
+
+void FetchJob::start()
+{
+    startJob(QLatin1String("/download/") + intListToString(mIdsPendingDownload));
+    setTotalAmount(KJob::Bytes, mTotalBytesToDownload);
+}
+
+void FetchJob::connectJob()
+{
+    SlaveBaseJob::connectJob();
+    connect(mJob, &KIO::TransferJob::infoMessage, this, &FetchJob::slotInfoMessage);
+}
+
+void FetchJob::slotSlaveData(KIO::Job *job, const QByteArray &data)
+{
+    Q_UNUSED(job);
+    mCurrentMessage += data;
+    mBytesDownloaded += data.size();
+    mDataCounter++;
+    if (mDataCounter % 5 == 0) {
+        setProcessedAmount(KJob::Bytes, mBytesDownloaded);
+    }
+}
+
+void FetchJob::slotInfoMessage(KJob *job, const QString &infoMessage, const QString &)
+{
+    Q_UNUSED(job);
+    if (infoMessage != QLatin1String("message complete")) {
+        return;
+    }
+
+    KMime::Message::Ptr msg(new KMime::Message);
+    msg->setContent(KMime::CRLFtoLF(mCurrentMessage));
+    msg->parse();
+
+    mCurrentMessage.clear();
+    const int idOfCurrentMessage = mIdsPendingDownload.takeFirst();
+    Q_EMIT messageFinished(idOfCurrentMessage, msg);
+}
+
diff --git a/resources/pop3/jobs.h b/resources/pop3/jobs.h
new file mode 100644 (file)
index 0000000..9a971f0
--- /dev/null
@@ -0,0 +1,200 @@
+/* Copyright 2009 Thomas McGuire <mcguire@kde.org>
+
+   This library is free software; you can redistribute it and/or modify
+   it under the terms of the GNU Library General Public License as published
+   by the Free Software Foundation; either version 2 of the License or
+   ( at your option ) version 3 or, at the discretion of KDE e.V.
+   ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version.
+
+   This library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Library General Public License for more details.
+
+   You should have received a copy of the GNU Library General Public License
+   along with this library; see the file COPYING.LIB.  If not, write to
+   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+   Boston, MA 02110-1301, USA.
+*/
+#ifndef JOBS_H
+#define JOBS_H
+
+#include <KJob>
+#include <QUrl>
+#include <KIO/MetaData>
+
+#include <KMime/Message>
+
+#include <QObject>
+#include <QPointer>
+
+namespace KIO
+{
+class Slave;
+class Job;
+class TransferJob;
+}
+
+class SlaveBaseJob;
+
+class POPSession : public QObject
+{
+    Q_OBJECT
+public:
+    explicit POPSession(const QString &password);
+    ~POPSession();
+    bool connectSlave();
+
+    void abortCurrentJob();
+    void closeSession();
+
+    KIO::Slave *getSlave() const;
+    QUrl getUrl() const;
+
+    // Sets the current SlaveBaseJob that is using the POPSession.
+    // If there is a job, all slave errors will be forwared to that job
+    void setCurrentJob(SlaveBaseJob *job);
+
+private Q_SLOTS:
+    void slotSlaveError(KIO::Slave *slave, int, const QString &);
+
+Q_SIGNALS:
+
+    // An error occurred within the slave. If there is a current job, this
+    // signal is not emitted, as the job deals with it.
+    void slaveError(int errorCode, const QString &errorMessage);
+
+private:
+    KIO::MetaData slaveConfig() const;
+    QString authenticationToString(int type) const;
+
+    QPointer<KIO::Slave> mSlave;
+    SlaveBaseJob *mCurrentJob;
+    QString mPassword;
+};
+
+class SlaveBaseJob : public KJob
+{
+    Q_OBJECT
+
+public:
+    explicit SlaveBaseJob(POPSession *POPSession);
+    ~SlaveBaseJob();
+
+    virtual void slaveError(int errorCode, const QString &errorMessage);
+
+protected Q_SLOTS:
+    virtual void slotSlaveData(KIO::Job *job, const QByteArray &data);
+    virtual void slotSlaveResult(KJob *job);
+
+protected:
+    QString errorString() const Q_DECL_OVERRIDE;
+    bool doKill() Q_DECL_OVERRIDE;
+    void startJob(const QString &path);
+    virtual void connectJob();
+
+    KIO::TransferJob *mJob;
+    POPSession *mPOPSession;
+};
+
+class LoginJob : public SlaveBaseJob
+{
+    Q_OBJECT
+public:
+    LoginJob(POPSession *popSession);
+    void start() Q_DECL_OVERRIDE;
+
+protected:
+    QString errorString() const Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void slaveConnected(KIO::Slave *slave);
+
+private:
+    void slaveError(int errorCode, const QString &errorMessage) Q_DECL_OVERRIDE;
+
+    QString mErrorString;
+};
+
+class ListJob : public SlaveBaseJob
+{
+    Q_OBJECT
+public:
+    ListJob(POPSession *popSession);
+    QMap<int, int> idList() const;
+    void start() Q_DECL_OVERRIDE;
+
+private:
+    void slotSlaveData(KIO::Job *job, const QByteArray &data) Q_DECL_OVERRIDE;
+
+private:
+    QMap<int, int> mIdList;
+};
+
+class UIDListJob : public SlaveBaseJob
+{
+    Q_OBJECT
+public:
+    UIDListJob(POPSession *popSession);
+    QMap<int, QString> uidList() const;
+    QMap<QString, int> idList() const;
+    void start() Q_DECL_OVERRIDE;
+
+private:
+    void slotSlaveData(KIO::Job *job, const QByteArray &data) Q_DECL_OVERRIDE;
+
+    QMap<int, QString> mUidList;
+    QMap<QString, int> mIdList;
+};
+
+class DeleteJob : public SlaveBaseJob
+{
+    Q_OBJECT
+public:
+    DeleteJob(POPSession *popSession);
+    void setDeleteIds(const QList<int> &ids);
+    void start() Q_DECL_OVERRIDE;
+    QList<int> deletedIDs() const;
+
+private:
+
+    QList<int> mIdsToDelete;
+};
+
+class QuitJob : public SlaveBaseJob
+{
+    Q_OBJECT
+
+public:
+    QuitJob(POPSession *popSession);
+    void start() Q_DECL_OVERRIDE;
+};
+
+class FetchJob : public SlaveBaseJob
+{
+    Q_OBJECT
+public:
+
+    FetchJob(POPSession *session);
+    void setFetchIds(const QList<int> &ids, const QList<int> &sizes);
+    void start() Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void slotInfoMessage(KJob *job, const QString &infoMessage, const QString &);
+
+Q_SIGNALS:
+    void messageFinished(int id, KMime::Message::Ptr message);
+
+private:
+
+    void connectJob() Q_DECL_OVERRIDE;
+    void slotSlaveData(KIO::Job *job, const QByteArray &data) Q_DECL_OVERRIDE;
+
+    QList<int> mIdsPendingDownload;
+    QByteArray mCurrentMessage;
+    int mBytesDownloaded;
+    int mTotalBytesToDownload;
+    uint mDataCounter;
+};
+
+#endif // JOBS_H
diff --git a/resources/pop3/metatype.h b/resources/pop3/metatype.h
new file mode 100644 (file)
index 0000000..e42abb8
--- /dev/null
@@ -0,0 +1,29 @@
+/* Copyright 2009 Thomas McGuire <mcguire@kde.org>
+
+   This library is free software; you can redistribute it and/or modify
+   it under the terms of the GNU Library General Public License as published
+   by the Free Software Foundation; either version 2 of the License or
+   ( at your option ) version 3 or, at the discretion of KDE e.V.
+   ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version.
+
+   This library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Library General Public License for more details.
+
+   You should have received a copy of the GNU Library General Public License
+   along with this library; see the file COPYING.LIB.  If not, write to
+   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+   Boston, MA 02110-1301, USA.
+*/
+#ifndef POP3_METATYPE_H
+#define POP3_METATYPE_H
+
+#include <QtCore/QObject>
+#include <QtCore/QList>
+
+#include <QtCore/QMetaType>
+
+//Q_DECLARE_METATYPE(QList<int>)
+
+#endif
diff --git a/resources/pop3/pop3resource.cpp b/resources/pop3/pop3resource.cpp
new file mode 100644 (file)
index 0000000..12ec7a7
--- /dev/null
@@ -0,0 +1,1009 @@
+/* Copyright 2009 Thomas McGuire <mcguire@kde.org>
+
+   This library is free software; you can redistribute it and/or modify
+   it under the terms of the GNU Library General Public License as published
+   by the Free Software Foundation; either version 2 of the License or
+   ( at your option ) version 3 or, at the discretion of KDE e.V.
+   ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version.
+
+   This library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Library General Public License for more details.
+
+   You should have received a copy of the GNU Library General Public License
+   along with this library; see the file COPYING.LIB.  If not, write to
+   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+   Boston, MA 02110-1301, USA.
+*/
+#include "pop3resource.h"
+#include "accountdialog.h"
+#include "settings.h"
+#include "jobs.h"
+#include <AkonadiCore/Pop3ResourceAttribute>
+
+#include <CollectionFetchJob>
+#include <ItemCreateJob>
+#include <AttributeFactory>
+#include <Akonadi/KMime/MessageFlags>
+#include <Akonadi/KMime/SpecialMailCollectionsRequestJob>
+#include <Akonadi/KMime/SpecialMailCollections>
+#include <kmime/kmime_util.h>
+#include <MailTransport/PrecommandJob>
+#include <MailTransport/Transport>
+
+#include <kio/global.h>
+#include <kio/job.h>
+#include <KPasswordDialog>
+#include <KMessageBox>
+#include <kwallet.h>
+#include "pop3resource_debug.h"
+
+#include <QTimer>
+
+using namespace Akonadi;
+using namespace MailTransport;
+using namespace KWallet;
+
+POP3Resource::POP3Resource(const QString &id)
+    : ResourceBase(id),
+      mState(Idle),
+      mPopSession(Q_NULLPTR),
+      mAskAgain(false),
+      mIntervalTimer(new QTimer(this)),
+      mTestLocalInbox(false),
+      mWallet(Q_NULLPTR),
+      idsToSaveValid(false),
+      mDeleteJob(Q_NULLPTR)
+{
+    Akonadi::AttributeFactory::registerAttribute<Akonadi::Pop3ResourceAttribute>();
+    setNeedsNetwork(true);
+    Settings::self()->setResourceId(identifier());
+    resetState();
+
+    connect(this, &POP3Resource::abortRequested, this, &POP3Resource::slotAbortRequested);
+    connect(mIntervalTimer, &QTimer::timeout,
+            this, &POP3Resource::intervalCheckTriggered);
+    connect(this, &POP3Resource::reloadConfiguration, this, &POP3Resource::configurationChanged);
+}
+
+POP3Resource::~POP3Resource()
+{
+    Settings::self()->save();
+    delete mWallet;
+    mWallet = Q_NULLPTR;
+}
+
+void POP3Resource::configurationChanged()
+{
+    Settings::self()->save();
+    updateIntervalTimer();
+}
+
+void POP3Resource::updateIntervalTimer()
+{
+    if (Settings::self()->intervalCheckEnabled() && mState == Idle) {
+        mIntervalTimer->start(Settings::self()->intervalCheckInterval() * 1000 * 60);
+    } else {
+        mIntervalTimer->stop();
+    }
+}
+
+void POP3Resource::intervalCheckTriggered()
+{
+    Q_ASSERT(mState == Idle);
+    if (isOnline()) {
+        qCDebug(POP3RESOURCE_LOG) << "Starting interval mail check.";
+        startMailCheck();
+        mIntervalCheckInProgress = true;
+    } else {
+        mIntervalTimer->start();
+    }
+}
+
+void POP3Resource::aboutToQuit()
+{
+    if (mState != Idle) {
+        cancelSync(i18n("Mail check aborted."));
+    }
+}
+
+void POP3Resource::slotAbortRequested()
+{
+    if (mState != Idle) {
+        cancelSync(i18n("Mail check was canceled manually."), false /* no error */);
+    }
+}
+
+void POP3Resource::configure(WId windowId)
+{
+    QPointer<AccountDialog> accountDialog(new AccountDialog(this, windowId));
+    if (accountDialog->exec() == QDialog::Accepted) {
+        updateIntervalTimer();
+        Q_EMIT configurationDialogAccepted();
+    } else {
+        Q_EMIT configurationDialogRejected();
+    }
+
+    delete accountDialog;
+}
+
+void POP3Resource::retrieveItems(const Akonadi::Collection &collection)
+{
+    Q_UNUSED(collection);
+    qCWarning(POP3RESOURCE_LOG) << "This should never be called, we don't have a collection!";
+}
+
+bool POP3Resource::retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts)
+{
+    Q_UNUSED(item);
+    Q_UNUSED(parts);
+    qCWarning(POP3RESOURCE_LOG) << "This should never be called, we don't have any item!";
+    return false;
+}
+
+QString POP3Resource::buildLabelForPasswordDialog(const QString &detailedError) const
+{
+    QString queryText = i18n("Please enter the username and password for account '%1'.",
+                             agentName());
+    queryText += QLatin1String("<br>") + detailedError;
+    return queryText;
+}
+
+void POP3Resource::walletOpenedForLoading(bool success)
+{
+    bool passwordLoaded = success;
+    if (success) {
+        if (mWallet && mWallet->isOpen() && mWallet->hasFolder(QStringLiteral("pop3"))) {
+            mWallet->setFolder(QStringLiteral("pop3"));
+            if (mWallet->hasEntry(identifier())) {
+                mWallet->readPassword(identifier(), mPassword);
+            } else {
+                passwordLoaded = false;
+            }
+        } else {
+            passwordLoaded = false;
+        }
+    }
+    delete mWallet;
+    mWallet = Q_NULLPTR;
+
+    if (!passwordLoaded) {
+        QString queryText = buildLabelForPasswordDialog(
+                                i18n("You are asked here because the password could not be loaded from the wallet."));
+        showPasswordDialog(queryText);
+    } else {
+        advanceState(Connect);
+    }
+}
+
+void POP3Resource::walletOpenedForSaving(bool success)
+{
+    if (success) {
+        if (mWallet && mWallet->isOpen()) {
+            if (!mWallet->hasFolder(QStringLiteral("pop3"))) {
+                mWallet->createFolder(QStringLiteral("pop3"));
+            }
+            mWallet->setFolder(QStringLiteral("pop3"));
+            mWallet->writePassword(identifier(), mPassword);
+        }
+    } else {
+        qCWarning(POP3RESOURCE_LOG) << "Unable to write the password to the wallet.";
+    }
+
+    delete mWallet;
+    mWallet = Q_NULLPTR;
+    finish();
+}
+
+void POP3Resource::showPasswordDialog(const QString &queryText)
+{
+    QPointer<KPasswordDialog> dlg =
+        new KPasswordDialog(
+        Q_NULLPTR,
+        KPasswordDialog::ShowUsernameLine);
+    dlg->setModal(true);
+    dlg->setUsername(Settings::self()->login());
+    dlg->setPassword(mPassword);
+    dlg->setPrompt(queryText);
+    dlg->setWindowTitle(name());
+    dlg->addCommentLine(i18n("Account:"), name());
+
+    bool gotIt = false;
+    if (dlg->exec()) {
+        mPassword = dlg->password();
+        Settings::self()->setLogin(dlg->username());
+        Settings::self()->save();
+        if (!dlg->password().isEmpty()) {
+            mSavePassword = true;
+        }
+
+        mAskAgain = false;
+        advanceState(Connect);
+        gotIt = true;
+    }
+    delete dlg;
+    if (!gotIt) {
+        cancelSync(i18n("No username and password supplied."));
+    }
+}
+
+void POP3Resource::advanceState(State nextState)
+{
+    mState = nextState;
+    doStateStep();
+}
+
+void POP3Resource::doStateStep()
+{
+    switch (mState) {
+    case Idle: {
+        Q_ASSERT(false);
+        qCWarning(POP3RESOURCE_LOG) << "State machine should not be called in idle state!";
+        break;
+    }
+    case FetchTargetCollection: {
+        qCDebug(POP3RESOURCE_LOG) << "================ Starting state FetchTargetCollection ==========";
+        Q_EMIT status(Running, i18n("Preparing transmission from \"%1\".", name()));
+        Collection targetCollection(Settings::self()->targetCollection());
+        if (!targetCollection.isValid()) {
+            // No target collection set in the config? Try requesting a default inbox
+            SpecialMailCollectionsRequestJob *requestJob = new SpecialMailCollectionsRequestJob(this);
+            requestJob->requestDefaultCollection(SpecialMailCollections::Inbox);
+            requestJob->start();
+            connect(requestJob, &SpecialMailCollectionsRequestJob::result, this, &POP3Resource::localFolderRequestJobFinished);
+        } else {
+            CollectionFetchJob *fetchJob = new CollectionFetchJob(targetCollection,
+                    CollectionFetchJob::Base);
+            fetchJob->start();
+            connect(fetchJob, &CollectionFetchJob::result, this, &POP3Resource::targetCollectionFetchJobFinished);
+        }
+        break;
+    }
+    case Precommand: {
+        qCDebug(POP3RESOURCE_LOG) << "================ Starting state Precommand =====================";
+        if (!Settings::self()->precommand().isEmpty()) {
+            PrecommandJob *precommandJob = new PrecommandJob(Settings::self()->precommand(), this);
+            connect(precommandJob, &PrecommandJob::result, this, &POP3Resource::precommandResult);
+            precommandJob->start();
+            Q_EMIT status(Running, i18n("Executing precommand."));
+        } else {
+            advanceState(RequestPassword);
+        }
+        break;
+    }
+    case RequestPassword: {
+        qCDebug(POP3RESOURCE_LOG) << "================ Starting state RequestPassword ================";
+
+        // Don't show any wallet or password prompts when we are unit-testing
+        if (!Settings::self()->unitTestPassword().isEmpty()) {
+            mPassword = Settings::self()->unitTestPassword();
+            advanceState(Connect);
+            break;
+        }
+
+        const bool passwordNeeded = Settings::self()->authenticationMethod() != MailTransport::Transport::EnumAuthenticationType::GSSAPI;
+        const bool loadPasswordFromWallet = !mAskAgain && passwordNeeded && !Settings::self()->login().isEmpty() &&
+                                            mPassword.isEmpty();
+        if (loadPasswordFromWallet) {
+            mWallet = Wallet::openWallet(Wallet::NetworkWallet(), winIdForDialogs(),
+                                         Wallet::Asynchronous);
+        }
+        if (loadPasswordFromWallet && mWallet) {
+            connect(mWallet, &KWallet::Wallet::walletOpened, this, &POP3Resource::walletOpenedForLoading);
+        } else if (passwordNeeded && (mPassword.isEmpty() || mAskAgain)) {
+            QString detail;
+            if (mAskAgain) {
+                detail = i18n("You are asked here because the previous login was not successful.");
+            } else if (Settings::self()->login().isEmpty()) {
+                detail = i18n("You are asked here because the username you supplied is empty.");
+            } else if (!mWallet) {
+                detail = i18n("You are asked here because the wallet password storage is disabled.");
+            }
+
+            showPasswordDialog(buildLabelForPasswordDialog(detail));
+        } else {
+            // No password needed or using previous password, go on with Connect
+            advanceState(Connect);
+        }
+
+        break;
+    }
+    case Connect: {
+        qCDebug(POP3RESOURCE_LOG) << "================ Starting state Connect ========================";
+        Q_ASSERT(!mPopSession);
+        mPopSession = new POPSession(mPassword);
+        connect(mPopSession, &POPSession::slaveError, this, &POP3Resource::slotSessionError);
+        advanceState(Login);
+        break;
+    }
+    case Login: {
+        qCDebug(POP3RESOURCE_LOG) << "================ Starting state Login ==========================";
+
+        LoginJob *loginJob = new LoginJob(mPopSession);
+        connect(loginJob, &LoginJob::result, this, &POP3Resource::loginJobResult);
+        loginJob->start();
+        break;
+    }
+    case List: {
+        qCDebug(POP3RESOURCE_LOG) << "================ Starting state List ===========================";
+        Q_EMIT status(Running, i18n("Fetching mail listing."));
+        ListJob *listJob = new ListJob(mPopSession);
+        connect(listJob, &ListJob::result, this, &POP3Resource::listJobResult);
+        listJob->start();
+    }
+    break;
+    case UIDList: {
+        qCDebug(POP3RESOURCE_LOG) << "================ Starting state UIDList ========================";
+        UIDListJob *uidListJob = new UIDListJob(mPopSession);
+        connect(uidListJob, &UIDListJob::result, this, &POP3Resource::uidListJobResult);
+        uidListJob->start();
+    }
+    break;
+    case Download: {
+        qCDebug(POP3RESOURCE_LOG) << "================ Starting state Download =======================";
+
+        // Determine which mails we want to download. Those are all mails which are
+        // currently on ther server, minus the ones we have already downloaded (we
+        // remember which UIDs we have downloaded in the settings)
+        QList<int> idsToDownload = mIdsToSizeMap.keys();
+        const QList<QString> alreadyDownloadedUIDs = Settings::self()->seenUidList();
+        foreach (const QString &uidOnServer, mIdsToUidsMap) {
+            if (alreadyDownloadedUIDs.contains(uidOnServer)) {
+                const int idOfUIDOnServer = mUidsToIdsMap.value(uidOnServer, -1);
+                Q_ASSERT(idOfUIDOnServer != -1);
+                idsToDownload.removeAll(idOfUIDOnServer);
+            }
+        }
+        mIdsToDownload = idsToDownload;
+        qCDebug(POP3RESOURCE_LOG) << "We are going to download" << mIdsToDownload.size() << "messages";
+
+        // For proper progress, the job needs to know the sizes of the messages, so
+        // put them into a list here
+        QList<int> sizesOfMessagesToDownload;
+        sizesOfMessagesToDownload.reserve(idsToDownload.count());
+        foreach (int id, idsToDownload) {
+            sizesOfMessagesToDownload.append(mIdsToSizeMap.value(id));
+        }
+
+        if (!mIdsToDownload.empty()) {
+            FetchJob *fetchJob = new FetchJob(mPopSession);
+            fetchJob->setFetchIds(idsToDownload, sizesOfMessagesToDownload);
+            connect(fetchJob, &FetchJob::result, this, &POP3Resource::fetchJobResult);
+            connect(fetchJob, &FetchJob::messageFinished, this, &POP3Resource::messageFinished);
+            connect(fetchJob, SIGNAL(processedAmount(KJob*,KJob::Unit,qulonglong)),
+                    this, SLOT(messageDownloadProgress(KJob*,KJob::Unit,qulonglong)));
+
+            fetchJob->start();
+        } else {
+            advanceState(Save);
+        }
+    }
+    break;
+    case Save: {
+        qCDebug(POP3RESOURCE_LOG) << "================ Starting state Save ===========================";
+        qCDebug(POP3RESOURCE_LOG) << mPendingCreateJobs.size() << "item create jobs are pending";
+        if (mPendingCreateJobs.size() > 0) {
+            Q_EMIT status(Running, i18n("Saving downloaded messages."));
+        }
+
+        if (shouldAdvanceToQuitState()) {
+            advanceState(Quit);
+        }
+    }
+    break;
+    case Quit: {
+        qCDebug(POP3RESOURCE_LOG) << "================ Starting state Quit ===========================";
+        QuitJob *quitJob = new QuitJob(mPopSession);
+        connect(quitJob, &QuitJob::result, this, &POP3Resource::quitJobResult);
+        quitJob->start();
+    }
+    break;
+    case SavePassword: {
+        qCDebug(POP3RESOURCE_LOG) << "================ Starting state SavePassword ===================";
+        if (!mSavePassword) {
+            finish();
+        } else {
+            qCDebug(POP3RESOURCE_LOG) << "Writing password back to the wallet.";
+            Q_EMIT status(Running, i18n("Saving password to the wallet."));
+            mWallet = Wallet::openWallet(Wallet::NetworkWallet(), winIdForDialogs(),
+                                         Wallet::Asynchronous);
+            if (mWallet) {
+                connect(mWallet, &KWallet::Wallet::walletOpened, this, &POP3Resource::walletOpenedForSaving);
+            } else {
+                finish();
+            }
+        }
+        break;
+    }
+    }
+}
+
+void POP3Resource::localFolderRequestJobFinished(KJob *job)
+{
+    if (job->error()) {
+        cancelSync(i18n("Error while trying to get the local inbox folder, "
+                        "aborting mail check.") + QLatin1Char('\n') + job->errorString());
+        return;
+    }
+    if (mTestLocalInbox) {
+        KMessageBox::information(Q_NULLPTR,
+                                 i18n("<qt>The folder you deleted was associated with the account "
+                                      "<b>%1</b> which delivered mail into it. The folder the account "
+                                      "delivers new mail into was reset to the main Inbox folder.</qt>", name()));
+    }
+    mTestLocalInbox = false;
+
+    mTargetCollection = SpecialMailCollections::self()->defaultCollection(SpecialMailCollections::Inbox);
+    Q_ASSERT(mTargetCollection.isValid());
+    advanceState(Precommand);
+}
+
+void POP3Resource::targetCollectionFetchJobFinished(KJob *job)
+{
+    if (job->error()) {
+        if (!mTestLocalInbox) {
+            mTestLocalInbox = true;
+            Settings::self()->setTargetCollection(-1);
+            advanceState(FetchTargetCollection);
+            return;
+        } else {
+            cancelSync(i18n("Error while trying to get the folder for incoming mail, "
+                            "aborting mail check.") + QLatin1Char('\n') + job->errorString());
+            mTestLocalInbox = false;
+            return;
+        }
+    }
+    mTestLocalInbox = false;
+    Akonadi::CollectionFetchJob *fetchJob =
+        dynamic_cast<Akonadi::CollectionFetchJob *>(job);
+    Q_ASSERT(fetchJob);
+    Q_ASSERT(fetchJob->collections().size() <= 1);
+
+    if (fetchJob->collections().isEmpty()) {
+        cancelSync(i18n("Could not find folder for incoming mail, aborting mail check."));
+        return;
+    } else {
+        mTargetCollection = fetchJob->collections().at(0);
+        advanceState(Precommand);
+    }
+}
+
+void POP3Resource::precommandResult(KJob *job)
+{
+    if (job->error()) {
+        cancelSync(i18n("Error while executing precommand.") +
+                   QLatin1Char('\n') + job->errorString());
+        return;
+    } else {
+        advanceState(RequestPassword);
+    }
+}
+
+void POP3Resource::loginJobResult(KJob *job)
+{
+    if (job->error()) {
+        qCDebug(POP3RESOURCE_LOG) << job->error() << job->errorText();
+        if (job->error() == KIO::ERR_COULD_NOT_LOGIN) {
+            mAskAgain = true;
+        }
+        cancelSync(i18n("Unable to login to the server %1.", Settings::self()->host()) +
+                   QLatin1Char('\n') + job->errorString());
+    } else {
+        advanceState(List);
+    }
+}
+
+void POP3Resource::listJobResult(KJob *job)
+{
+    if (job->error()) {
+        cancelSync(i18n("Error while getting the list of messages on the server.") +
+                   QLatin1Char('\n') + job->errorString());
+    } else {
+        ListJob *listJob = dynamic_cast<ListJob *>(job);
+        Q_ASSERT(listJob);
+        mIdsToSizeMap = listJob->idList();
+        idsToSaveValid = false;
+        qCDebug(POP3RESOURCE_LOG) << "IdsToSizeMap:" << mIdsToSizeMap;
+        advanceState(UIDList);
+    }
+}
+
+void POP3Resource::uidListJobResult(KJob *job)
+{
+    if (job->error()) {
+        cancelSync(i18n("Error while getting list of unique mail identifiers from the server.") +
+                   QLatin1Char('\n') + job->errorString());
+    } else {
+        UIDListJob *listJob = dynamic_cast<UIDListJob *>(job);
+        Q_ASSERT(listJob);
+        mIdsToUidsMap = listJob->uidList();
+        mUidsToIdsMap = listJob->idList();
+        qCDebug(POP3RESOURCE_LOG) << "IdToUidMap:" << mIdsToUidsMap;
+        qCDebug(POP3RESOURCE_LOG) << "UidToIdMap:" << mUidsToIdsMap;
+
+        mUidListValid = !mIdsToUidsMap.isEmpty() || mIdsToSizeMap.isEmpty();
+        if (Settings::self()->leaveOnServer() && !mUidListValid) {
+            // FIXME: this needs a proper parent window
+            KMessageBox::sorry(Q_NULLPTR,
+                               i18n("Your POP3 server (Account: %1) does not support "
+                                    "the UIDL command: this command is required to determine, in a reliable way, "
+                                    "which of the mails on the server KMail has already seen before;\n"
+                                    "the feature to leave the mails on the server will therefore not "
+                                    "work properly.", name()));
+        }
+
+        advanceState(Download);
+    }
+}
+
+void POP3Resource::fetchJobResult(KJob *job)
+{
+    if (job->error()) {
+        cancelSync(i18n("Error while fetching mails from the server.") +
+                   QLatin1Char('\n') + job->errorString());
+        return;
+    } else {
+        qCDebug(POP3RESOURCE_LOG) << "Downloaded" << mDownloadedIDs.size() << "mails";
+
+        if (!mIdsToDownload.isEmpty()) {
+            qCWarning(POP3RESOURCE_LOG) << "We did not download all messages, there are still some remaining "
+                                        "IDs, even though we requested their download:" << mIdsToDownload;
+        }
+
+        advanceState(Save);
+    }
+}
+
+void POP3Resource::messageFinished(int messageId, KMime::Message::Ptr message)
+{
+    if (mState != Download) {
+        // This can happen if the slave does not get notified in time about the fact
+        // that the job was killed
+        return;
+    }
+
+    //qCDebug(POP3RESOURCE_LOG) << "Got message" << messageId
+    //         << "with subject" << message->subject()->asUnicodeString();
+
+    Akonadi::Item item;
+    item.setMimeType(QStringLiteral("message/rfc822"));
+    item.setPayload<KMime::Message::Ptr>(message);
+
+    Akonadi::Pop3ResourceAttribute *attr  = item.attribute<Akonadi::Pop3ResourceAttribute>(Akonadi::Item::AddIfMissing);
+    attr->setPop3AccountName(identifier());
+    Akonadi::MessageFlags::copyMessageFlags(*message, item);
+    ItemCreateJob *itemCreateJob = new ItemCreateJob(item, mTargetCollection);
+
+    mPendingCreateJobs.insert(itemCreateJob, messageId);
+    connect(itemCreateJob, &ItemCreateJob::result, this, &POP3Resource::itemCreateJobResult);
+
+    mDownloadedIDs.append(messageId);
+    mIdsToDownload.removeAll(messageId);
+}
+
+void POP3Resource::messageDownloadProgress(KJob *job, KJob::Unit unit, qulonglong totalBytes)
+{
+    Q_UNUSED(totalBytes);
+    Q_UNUSED(unit);
+    Q_ASSERT(unit == KJob::Bytes);
+    QString statusMessage;
+    const int totalMessages = mIdsToDownload.size() + mDownloadedIDs.size();
+    int bytesRemainingOnServer = 0;
+    foreach (const QString &alreadyDownloadedUID, Settings::self()->seenUidList()) {
+        const int alreadyDownloadedID = mUidsToIdsMap.value(alreadyDownloadedUID, -1);
+        if (alreadyDownloadedID != -1) {
+            bytesRemainingOnServer += mIdsToSizeMap.value(alreadyDownloadedID);
+        }
+    }
+
+    if (Settings::self()->leaveOnServer() && bytesRemainingOnServer > 0) {
+
+        statusMessage = i18n("Fetching message %1 of %2 (%3 of %4 KB) for %5 "
+                             "(%6 KB remain on the server).",
+                             mDownloadedIDs.size() + 1, totalMessages,
+                             job->processedAmount(KJob::Bytes) / 1024,
+                             job->totalAmount(KJob::Bytes) / 1024, name(),
+                             bytesRemainingOnServer / 1024);
+    } else {
+        statusMessage = i18n("Fetching message %1 of %2 (%3 of %4 KB) for %5",
+                             mDownloadedIDs.size() + 1, totalMessages,
+                             job->processedAmount(KJob::Bytes) / 1024,
+                             job->totalAmount(KJob::Bytes) / 1024, name());
+    }
+    Q_EMIT status(Running, statusMessage);
+    Q_EMIT percent(job->percent());
+}
+
+void POP3Resource::itemCreateJobResult(KJob *job)
+{
+    if (mState != Download && mState != Save) {
+        // This can happen if the slave does not get notified in time about the fact
+        // that the job was killed
+        return;
+    }
+
+    ItemCreateJob *createJob = dynamic_cast<ItemCreateJob *>(job);
+    Q_ASSERT(createJob);
+
+    if (job->error()) {
+        cancelSync(i18n("Unable to store downloaded mails.") +
+                   QLatin1Char('\n') + job->errorString());
+        return;
+    }
+
+    const int idOfMessageJustCreated = mPendingCreateJobs.value(createJob, -1);
+    Q_ASSERT(idOfMessageJustCreated != -1);
+    mPendingCreateJobs.remove(createJob);
+    mIDsStored.append(idOfMessageJustCreated);
+    //qCDebug(POP3RESOURCE_LOG) << "Just stored message with ID" << idOfMessageJustCreated
+    //         << "on the Akonadi server";
+
+    if (shouldDeleteId(idOfMessageJustCreated)) {
+        mIdsWaitingToDelete << idOfMessageJustCreated;
+        if (!mDeleteJob) {
+            mDeleteJob = new DeleteJob(mPopSession);
+            mDeleteJob->setDeleteIds(mIdsWaitingToDelete);
+            mIdsWaitingToDelete.clear();
+            connect(mDeleteJob, &DeleteJob::result, this, &POP3Resource::deleteJobResult);
+            mDeleteJob->start();
+        }
+    }
+
+    // Have all create jobs finished? Go to the next state, then
+    if (shouldAdvanceToQuitState()) {
+        advanceState(Quit);
+    }
+}
+
+int POP3Resource::idToTime(int id) const
+{
+    const QString uid = mIdsToUidsMap.value(id);
+    if (!uid.isEmpty()) {
+        const int index = Settings::self()->seenUidList().indexOf(uid);
+        if (index != -1) {
+            return Settings::self()->seenUidTimeList().at(index);
+        }
+    }
+
+    // If we don't find any mail, either we have no UID, or it is not in the seen UID
+    // list. In that case, we assume that the mail is new, i.e. from now
+    return time(Q_NULLPTR);
+}
+
+int POP3Resource::idOfOldestMessage(const QSet<int> &idList) const
+{
+    int timeOfOldestMessage = time(Q_NULLPTR) + 999;
+    int idOfOldestMessage = -1;
+    foreach (int id, idList) {
+        const int idTime = idToTime(id);
+        if (idTime < timeOfOldestMessage) {
+            timeOfOldestMessage = idTime;
+            idOfOldestMessage = id;
+        }
+    }
+    Q_ASSERT(idList.isEmpty() || idOfOldestMessage != -1);
+    return idOfOldestMessage;
+}
+
+bool POP3Resource::shouldDeleteId(int downloadedId) const
+{
+    // By default, we delete all messages. But if we have "leave on server"
+    // rules, we can save some messages.
+    if (Settings::self()->leaveOnServer()) {
+        if (!idsToSaveValid) {
+            idsToSaveValid = true;
+            idsToSave = QSet<int>();
+
+            const QSet<int> idsOnServer = QSet<int>::fromList(mIdsToSizeMap.keys());
+
+            // If the time-limited leave rule is checked, add the newer messages to
+            // the list of messages to keep
+            if (Settings::self()->leaveOnServerDays() > 0) {
+                const int secondsPerDay = 86400;
+                time_t timeLimit = time(Q_NULLPTR) - (secondsPerDay * Settings::self()->leaveOnServerDays());
+                foreach (int idToDelete, idsOnServer) {
+                    const int msgTime = idToTime(idToDelete);
+                    if (msgTime >= timeLimit) {
+                        idsToSave << idToDelete;
+                    } else {
+                        qCDebug(POP3RESOURCE_LOG) << "Message" << idToDelete << "is too old and will be deleted.";
+                    }
+                }
+            }
+
+            // Otherwise, add all messages to the list of messages to keep - this may
+            // be reduced in the following number-limited leave rule and size-limited
+            // leave rule checks
+            else {
+                idsToSave = idsOnServer;
+            }
+
+            //
+            // Delete more old messages if there are more than mLeaveOnServerCount
+            //
+            if (Settings::self()->leaveOnServerCount() > 0) {
+                const int numToDelete = idsToSave.count() - Settings::self()->leaveOnServerCount();
+                if (numToDelete > 0 && numToDelete < idsToSave.count()) {
+                    // Get rid of the first numToDelete messages
+                    for (int i = 0; i < numToDelete; i++) {
+                        idsToSave.remove(idOfOldestMessage(idsToSave));
+                    }
+                } else if (numToDelete >= idsToSave.count()) {
+                    idsToSave.clear();
+                }
+            }
+
+            //
+            // Delete more old messages until we're under mLeaveOnServerSize MBs
+            //
+            if (Settings::self()->leaveOnServerSize() > 0) {
+                const qint64 limitInBytes = Settings::self()->leaveOnServerSize() * (1024 * 1024);
+                qint64 sizeOnServerAfterDeletion = 0;
+                foreach (int id, idsToSave) {
+                    sizeOnServerAfterDeletion += mIdsToSizeMap.value(id);
+                }
+                while (sizeOnServerAfterDeletion > limitInBytes) {
+                    int oldestId = idOfOldestMessage(idsToSave);
+                    idsToSave.remove(oldestId);
+                    sizeOnServerAfterDeletion -= mIdsToSizeMap.value(oldestId);
+                }
+            }
+        }
+
+        return !idsToSave.contains(downloadedId);
+    }
+
+    return true;
+}
+
+void POP3Resource::deleteJobResult(KJob *job)
+{
+    if (job->error()) {
+        cancelSync(i18n("Failed to delete the messages from the server.") +
+                   QLatin1Char('\n') + job->errorString());
+        return;
+    }
+
+    DeleteJob *finishedDeleteJob = dynamic_cast<DeleteJob *>(job);
+    Q_ASSERT(finishedDeleteJob);
+    Q_ASSERT(finishedDeleteJob == mDeleteJob);
+    mDeletedIDs = finishedDeleteJob->deletedIDs();
+
+    // Remove all deleted messages from the list of already downloaded messages,
+    // as it is no longer necessary to store them (they just waste space)
+    QList<QString> seenUIDs = Settings::self()->seenUidList();
+    QList<int> timeOfSeenUids = Settings::self()->seenUidTimeList();
+    Q_ASSERT(seenUIDs.size() == timeOfSeenUids.size());
+    foreach (int deletedId, mDeletedIDs) {
+        QString deletedUID = mIdsToUidsMap.value(deletedId);
+        if (!deletedUID.isEmpty()) {
+            int index = seenUIDs.indexOf(deletedUID);
+            if (index != -1) {
+                // TEST
+                qCDebug(POP3RESOURCE_LOG) << "Removing UID" << deletedUID << "from the seen UID list, as it was deleted.";
+                seenUIDs.removeAt(index);
+                timeOfSeenUids.removeAt(index);
+            }
+        }
+    }
+    Settings::self()->setSeenUidList(seenUIDs);
+    Settings::self()->setSeenUidTimeList(timeOfSeenUids);
+    Settings::self()->save();
+
+    mDeleteJob = Q_NULLPTR;
+    if (!mIdsWaitingToDelete.isEmpty()) {
+        mDeleteJob = new DeleteJob(mPopSession);
+        mDeleteJob->setDeleteIds(mIdsWaitingToDelete);
+        mIdsWaitingToDelete.clear();
+        connect(mDeleteJob, &DeleteJob::result, this, &POP3Resource::deleteJobResult);
+        mDeleteJob->start();
+    }
+
+    if (shouldAdvanceToQuitState()) {
+             advanceState(Quit);
+    }
+}
+
+void POP3Resource::finish()
+{
+    qCDebug(POP3RESOURCE_LOG) << "================= Mail check finished. =============================";
+    saveSeenUIDList();
+    if (!mIntervalCheckInProgress) {
+        collectionsRetrieved(Akonadi::Collection::List());
+    }
+    if (mDownloadedIDs.isEmpty()) {
+        Q_EMIT status(Idle, i18n("Finished mail check, no message downloaded."));
+    } else
+        Q_EMIT status(Idle, i18np("Finished mail check, 1 message downloaded.",
+                                  "Finished mail check, %1 messages downloaded.",
+                                  mDownloadedIDs.size()));
+
+    resetState();
+}
+
+bool POP3Resource::shouldAdvanceToQuitState() const
+{
+    return mState == Save && mPendingCreateJobs.isEmpty() && mIdsWaitingToDelete.isEmpty() && !mDeleteJob;
+}
+
+void POP3Resource::quitJobResult(KJob *job)
+{
+    if (job->error()) {
+        cancelSync(i18n("Unable to complete the mail fetch.") +
+                   QLatin1Char('\n') + job->errorString());
+        return;
+    }
+
+    advanceState(SavePassword);
+}
+
+void POP3Resource::slotSessionError(int errorCode, const QString &errorMessage)
+{
+    qCWarning(POP3RESOURCE_LOG) << "Error in our session, unrelated to a currently running job!";
+    cancelSync(KIO::buildErrorString(errorCode, errorMessage));
+}
+
+void POP3Resource::saveSeenUIDList()
+{
+    QList<QString> seenUIDs = Settings::self()->seenUidList();
+    QList<int> timeOfSeenUIDs = Settings::self()->seenUidTimeList();
+
+    //
+    // Find the messages that we have successfully stored, but did not actually get
+    // deleted.
+    // Those messages, we have to remember, so we don't download them again.
+    //
+    QList<int> idsOfMessagesDownloadedButNotDeleted = mIDsStored;
+    foreach (int deletedId, mDeletedIDs) {
+        idsOfMessagesDownloadedButNotDeleted.removeAll(deletedId);
+    }
+    QList<QString> uidsOfMessagesDownloadedButNotDeleted;
+    foreach (int id, idsOfMessagesDownloadedButNotDeleted) {
+        QString uid = mIdsToUidsMap.value(id);
+        if (!uid.isEmpty()) {
+            uidsOfMessagesDownloadedButNotDeleted.append(uid);
+        }
+    }
+    Q_ASSERT(seenUIDs.size() == timeOfSeenUIDs.size());
+    foreach (const QString &uid, uidsOfMessagesDownloadedButNotDeleted) {
+        if (!seenUIDs.contains(uid)) {
+            seenUIDs.append(uid);
+            timeOfSeenUIDs.append(time(Q_NULLPTR));
+        }
+    }
+
+    //
+    // If we have a valid UID list from the server, delete those UIDs that are in
+    // the seenUidList but are not on the server.
+    // This can happen if someone else than this resource deleted the mails from the
+    // server which we kept here.
+    //
+    if (mUidListValid) {
+        QList<QString>::iterator uidIt = seenUIDs.begin();
+        QList<int>::iterator timeIt = timeOfSeenUIDs.begin();
+        while (uidIt != seenUIDs.end()) {
+            const QString curSeenUID = *uidIt;
+            if (!mUidsToIdsMap.contains(curSeenUID)) {
+                // Ok, we have a UID in the seen UID list that is not anymore on the server.
+                // Therefore remove it from the seen UID list, it is not needed there anymore,
+                // it just wastes space.
+                uidIt = seenUIDs.erase(uidIt);
+                timeIt = timeOfSeenUIDs.erase(timeIt);
+            } else {
+                ++uidIt;
+                ++timeIt;
+            }
+        }
+    } else {
+        qCWarning(POP3RESOURCE_LOG) << "UID list from server is not valid.";
+    }
+
+    //
+    // Now save it in the settings
+    //
+    qCDebug(POP3RESOURCE_LOG) << "The seen UID list has" << seenUIDs.size() << "entries";
+    Settings::self()->setSeenUidList(seenUIDs);
+    Settings::self()->setSeenUidTimeList(timeOfSeenUIDs);
+    Settings::self()->save();
+}
+
+void POP3Resource::cancelSync(const QString &errorMessage, bool error)
+{
+    if (error) {
+        cancelTask(errorMessage);
+        qCWarning(POP3RESOURCE_LOG) << "============== ERROR DURING POP3 SYNC ==========================";
+        qCWarning(POP3RESOURCE_LOG) << errorMessage;
+    } else {
+        qCDebug(POP3RESOURCE_LOG) << "Canceled the sync, but no error.";
+        cancelTask();
+    }
+    saveSeenUIDList();
+    resetState();
+}
+
+void POP3Resource::resetState()
+{
+    mState = Idle;
+    mTargetCollection = Collection(-1);
+    mIdsToSizeMap.clear();
+    mIdsToUidsMap.clear();
+    mUidsToIdsMap.clear();
+    mDownloadedIDs.clear();
+    mIdsToDownload.clear();
+    mPendingCreateJobs.clear();
+    mIDsStored.clear();
+    mDeletedIDs.clear();
+    mIdsWaitingToDelete.clear();
+    if (mDeleteJob) {
+        mDeleteJob->deleteLater();
+        mDeleteJob = Q_NULLPTR;
+    }
+    mUidListValid = false;
+    mIntervalCheckInProgress = false;
+    mSavePassword = false;
+    updateIntervalTimer();
+    delete mWallet;
+    mWallet = Q_NULLPTR;
+
+    if (mPopSession) {
+        // Closing the POP session means the KIO slave will get disconnected, which
+        // automatically issues the QUIT command.
+        // Delete the POP session later, otherwise the scheduler makes us crash
+        mPopSession->abortCurrentJob();
+        mPopSession->deleteLater();
+        mPopSession = Q_NULLPTR;
+    }
+}
+
+void POP3Resource::startMailCheck()
+{
+    resetState();
+    mIntervalTimer->stop();
+    Q_EMIT percent(0);   // Otherwise the value from the last sync is taken
+    advanceState(FetchTargetCollection);
+}
+
+void POP3Resource::retrieveCollections()
+{
+    if (mState == Idle) {
+        startMailCheck();
+    } else {
+        cancelSync(
+            i18n("Mail check already in progress, unable to start a second check."));
+    }
+}
+
+void POP3Resource::clearCachedPassword()
+{
+    mPassword.clear();
+}
+
+void POP3Resource::cleanup()
+{
+    if (mWallet && mWallet->isOpen() && mWallet->hasFolder(QStringLiteral("pop3"))) {
+        mWallet->setFolder(QStringLiteral("pop3"));
+        if (mWallet->hasEntry(identifier())) {
+            mWallet->removeEntry(identifier());
+        }
+    }
+    Akonadi::AgentBase::cleanup();
+}
+
+void POP3Resource::doSetOnline(bool online)
+{
+    ResourceBase::doSetOnline(online);
+    if (online) {
+        Q_EMIT status(Idle, i18n("Ready"));
+    } else {
+        if (mState != Idle) {
+            cancelSync(i18n("Mail check aborted after going offline."), false /* no error */);
+        }
+        Q_EMIT status(Idle, i18n("Offline"));
+        delete mWallet;
+        mWallet = Q_NULLPTR;
+        clearCachedPassword();
+    }
+}
+
+AKONADI_RESOURCE_MAIN(POP3Resource)
diff --git a/resources/pop3/pop3resource.desktop b/resources/pop3/pop3resource.desktop
new file mode 100644 (file)
index 0000000..912640f
--- /dev/null
@@ -0,0 +1,102 @@
+[Desktop Entry]
+Name=POP3 E-Mail Server
+Name[bg]=Пощенски сървър POP3
+Name[bs]=POP3 E-Mail Server
+Name[ca]=Servidor de correu POP3
+Name[ca@valencia]=Servidor de correu POP3
+Name[cs]=E-mailový POP3 server
+Name[da]=POP3 e-mail-server
+Name[de]=POP3-E-Mail-Server
+Name[el]=Εξυπηρετητής ηλ.ταχυδρομείου POP3
+Name[en_GB]=POP3 E-Mail Server
+Name[es]=Servidor de correo POP-3
+Name[et]=POP3 e-posti server
+Name[fi]=POP3-sähköpostipalvelin
+Name[fr]=Serveur de courriers électroniques POP3
+Name[ga]=Freastalaí Ríomhphoist POP3
+Name[gl]=Servidor de correo electrónico de POP3
+Name[hu]=POP3 e-mail kiszolgáló
+Name[ia]=Servitor POP3 de e-posta
+Name[it]=Server di posta POP3
+Name[ja]=POP3 メールサーバ
+Name[kk]=POP3 эл.пошта сервері
+Name[km]=ម៉ាស៊ីន​បម្រើ​អ៊ីមែល POP3
+Name[ko]=POP3 이메일 서버
+Name[lt]=POP3 el. pašto serveris
+Name[lv]=POP3 e-pasta serveris
+Name[nb]=POP3 E-post-tjener
+Name[nds]=POP3-Nettpostserver
+Name[nl]=POP3 e-mailserver
+Name[nn]=POP3-basert e-posttenar
+Name[pa]=POP3 ਈਮੇਲ ਸਰਵਰ
+Name[pl]=Serwer pocztowy POP3
+Name[pt]=Servidor de E-Mail POP3
+Name[pt_BR]=Servidor de e-mails POP3
+Name[ro]=Server de poștă POP3
+Name[ru]=Почтовый сервер POP3
+Name[sk]=POP3 poštový server
+Name[sl]=E-poštni strežnik POP3
+Name[sr]=ПОП3 сервер е‑поште
+Name[sr@ijekavian]=ПОП3 сервер е‑поште
+Name[sr@ijekavianlatin]=POP3 server e‑pošte
+Name[sr@latin]=POP3 server e‑pošte
+Name[sv]=POP3 e-postserver
+Name[tr]=POP3 E-Posta Sunucu
+Name[uk]=Сервер пошти POP3
+Name[x-test]=xxPOP3 E-Mail Serverxx
+Name[zh_CN]=POP3 电子邮件服务器
+Name[zh_TW]=POP3 信件伺服器
+Comment=Connects to a POP3 e-mail server
+Comment[bg]=Свързване с пощенски сървър POP3
+Comment[bs]=POvezuje se na POP3 e-mail server
+Comment[ca]=Connecta a un servidor de correu POP3
+Comment[ca@valencia]=Connecta a un servidor de correu POP3
+Comment[cs]=Připojí se na e-mailový POP3 server
+Comment[da]=Forbinder til en POP3 e-mail-server
+Comment[de]=Verbindet zu einem POP3-E-Mail-Server.
+Comment[el]=Συνδέεται σε έναν εξυπηρετητή ηλ.ταχυδρομείου POP3.
+Comment[en_GB]=Connects to a POP3 e-mail server
+Comment[es]=Conecta a un servidor de correo POP3
+Comment[et]=Ühendumine POP3 e-posti serveriga
+Comment[fi]=Yhdistää POP3-sähköpostipalvelimeen.
+Comment[fr]=Se connecte à un serveur de courriers électroniques POP3
+Comment[ga]=Déanann sé seo ceangal le freastalaí ríomhphoist POP3
+Comment[gl]=Conéctase a un servidor de correo POP3
+Comment[hu]=Kapcsolódás egy POP3 e-mail kiszolgálóhoz
+Comment[ia]=Connecte a un servitor POP3 de e-posta
+Comment[it]=Si collega ad un server di posta POP3.
+Comment[ja]=POP3 のメールサーバに接続します。
+Comment[kk]=POP3 эл.пошта серверімен байланыс құру
+Comment[km]=តភ្ជាប់​ទៅកាន់​ម៉ាស៊ីន​បម្រើ​អ៊ីមែល POP3
+Comment[ko]=POP3 이메일 서버에 연결함
+Comment[lt]=Jungiasi prie POP3 el. pašto serverio
+Comment[lv]=Pieslēdzas POP3 e-pasta serverim
+Comment[nb]=Kobler til en POP3 e-posttjener.
+Comment[nds]=Stellt en Verbinnen na en POP3-Nettpostserver op.
+Comment[nl]=Maakt verbinding met een POP3 e-mailserver.
+Comment[nn]=Koplar til ein POP3-basert e-posttenar
+Comment[pa]=POP3 ਈਮੇਲ ਸਰਵਰ ਨਾਲ ਕੁਨੈਕਟ ਕਰਦਾ ਹੈ।
+Comment[pl]=Łączy z serwerem poczty POP3
+Comment[pt]=Liga-se a um servidor de e-mail em POP3
+Comment[pt_BR]=Conecta a um servidor de e-mails POP3
+Comment[ro]=Se conectează la un server de poștă POP3
+Comment[ru]=Подключение к почтовому серверу POP3
+Comment[sk]=Pripojí sa na POP3 poštový server
+Comment[sl]=Poveže se z e-poštnim strežnikom POP3
+Comment[sr]=Повезује се на ПОП3 сервер е‑поште
+Comment[sr@ijekavian]=Повезује се на ПОП3 сервер е‑поште
+Comment[sr@ijekavianlatin]=Povezuje se na POP3 server e‑pošte
+Comment[sr@latin]=Povezuje se na POP3 server e‑pošte
+Comment[sv]=Ansluter till en POP3 e-postserver.
+Comment[tr]=POP3 e-posta sunucusuna bağlanır
+Comment[uk]=Встановлює з’єднання з поштовим сервером POP3.
+Comment[x-test]=xxConnects to a POP3 e-mail serverxx
+Comment[zh_CN]=连接到一个 POP3 电子邮件服务器
+Comment[zh_TW]=連線到 POP3 信件伺服器
+Type=AkonadiResource
+Exec=akonadi_pop3_resource
+Icon=network-server
+
+X-Akonadi-MimeTypes=message/rfc822
+X-Akonadi-Capabilities=Resource,NeedsNetwork
+X-Akonadi-Identifier=akonadi_pop3_resource
diff --git a/resources/pop3/pop3resource.h b/resources/pop3/pop3resource.h
new file mode 100644 (file)
index 0000000..0558b82
--- /dev/null
@@ -0,0 +1,203 @@
+/* Copyright 2009 Thomas McGuire <mcguire@kde.org>
+
+   This library is free software; you can redistribute it and/or modify
+   it under the terms of the GNU Library General Public License as published
+   by the Free Software Foundation; either version 2 of the License or
+   ( at your option ) version 3 or, at the discretion of KDE e.V.
+   ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version.
+
+   This library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Library General Public License for more details.
+
+   You should have received a copy of the GNU Library General Public License
+   along with this library; see the file COPYING.LIB.  If not, write to
+   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+   Boston, MA 02110-1301, USA.
+*/
+#ifndef POP3RESOURCE_H
+#define POP3RESOURCE_H
+
+#include <ResourceBase>
+#include <KMime/Message>
+#include <KJob>
+#include <QSet>
+
+class DeleteJob;
+
+namespace Akonadi
+{
+class ItemCreateJob;
+}
+namespace KWallet
+{
+class Wallet;
+}
+
+class POPSession;
+class QTimer;
+
+class POP3Resource : public Akonadi::ResourceBase,
+    public Akonadi::AgentBase::Observer
+{
+    Q_OBJECT
+
+public:
+    POP3Resource(const QString &id);
+    ~POP3Resource();
+
+    void clearCachedPassword();
+
+    void cleanup() Q_DECL_OVERRIDE;
+
+public Q_SLOTS:
+    void configure(WId windowId) Q_DECL_OVERRIDE;
+
+protected Q_SLOTS:
+    void retrieveCollections() Q_DECL_OVERRIDE;
+    void retrieveItems(const Akonadi::Collection &col) Q_DECL_OVERRIDE;
+    bool retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+
+protected:
+
+    void aboutToQuit() Q_DECL_OVERRIDE;
+    void doSetOnline(bool online) Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+
+    void slotAbortRequested();
+    void intervalCheckTriggered();
+    void configurationChanged();
+
+    // Error unrelated to a state
+    void slotSessionError(int errorCode, const QString &errorMessage);
+
+    // For state FetchTargetCollection
+    void targetCollectionFetchJobFinished(KJob *job);
+    void localFolderRequestJobFinished(KJob *job);
+
+    // For state Precommand
+    void precommandResult(KJob *job);
+
+    // For state RequestPassword
+    void walletOpenedForLoading(bool success);
+
+    // For state Login
+    void loginJobResult(KJob *job);
+
+    // For state List
+    void listJobResult(KJob *job);
+
+    // For state UIDList
+    void uidListJobResult(KJob *job);
+
+    // For state Download
+    void messageFinished(int messageId, KMime::Message::Ptr message);
+    void fetchJobResult(KJob *job);
+    void messageDownloadProgress(KJob *job, KJob::Unit unit, qulonglong totalBytes);
+
+    // For state Save
+    void itemCreateJobResult(KJob *job);
+
+    // For state Delete
+    void deleteJobResult(KJob *job);
+
+    // For state Quit
+    void quitJobResult(KJob *job);
+
+    // For state SavePassword
+    void walletOpenedForSaving(bool success);
+
+private:
+
+    enum State {
+        Idle,
+        FetchTargetCollection,
+        Precommand,
+        RequestPassword,
+        Connect,
+        Login,
+        List,
+        UIDList,
+        Download,
+        Save,
+        Quit,
+        SavePassword
+    };
+
+    void resetState();
+    void doStateStep();
+    void advanceState(State nextState);
+    void cancelSync(const QString &errorMessage, bool error = true);
+    void saveSeenUIDList();
+    bool shouldDeleteId(int downloadedId) const;
+    int idToTime(int id) const;
+    int idOfOldestMessage(const QSet<int> &idList) const;
+    void startMailCheck();
+    void updateIntervalTimer();
+    void showPasswordDialog(const QString &queryText);
+    QString buildLabelForPasswordDialog(const QString &detailedError) const;
+    void finish();
+
+    bool shouldAdvanceToQuitState() const;
+
+    State mState;
+    Akonadi::Collection mTargetCollection;
+    POPSession *mPopSession;
+    bool mAskAgain;
+    QTimer *mIntervalTimer;
+    bool mIntervalCheckInProgress;
+    QString mPassword;
+    bool mSavePassword;
+    bool mTestLocalInbox;
+    KWallet::Wallet *mWallet;
+
+    // Maps IDs on the server to message sizes on the server
+    QMap<int, int> mIdsToSizeMap;
+
+    // Maps IDs on the server to UIDs on the server.
+    // This can be empty, if the server doesn't support UIDL
+    QMap<int, QString> mIdsToUidsMap;
+
+    // Maps UIDs on the server to IDs on the server.
+    // This can be empty, if the server doesn't support UIDL
+    QMap<QString, int> mUidsToIdsMap;
+
+    // Whether we actually received a valid UID list from the server
+    bool mUidListValid;
+
+    // IDs of messages that we have successfully downloaded. This does _not_ mean
+    // that the messages corresponding to the IDs are stored in Akonadi yet
+    QList<int> mDownloadedIDs;
+
+    // IDs of messages that we want to download and that we have started the
+    // FetchJob with. After the FetchJob, this should be empty, except if there
+    // was some error
+    QList<int> mIdsToDownload;
+
+    // After downloading a message, we store it in Akonadi by using an ItemCreateJob.
+    // This map stores the currently running ItemCreateJob's and their corresponding
+    // POP3 IDs.
+    // When an ItemCreateJob finished, it is removed from this map.
+    // The Save state waits until this map becomes empty.
+    QMap<Akonadi::ItemCreateJob *, int> mPendingCreateJobs;
+
+    // List of message IDs that were successfully stored in Akonadi
+    QList<int> mIDsStored;
+
+    // List of message IDs that were successfully deleted
+    QList<int> mDeletedIDs;
+
+    // List of message IDs that we want to delete with the next delete job
+    QList<int> mIdsWaitingToDelete;
+
+    // List of message IDs that we want to keep on the server
+    mutable QSet<int> idsToSave;
+    mutable bool idsToSaveValid;
+
+    // Current deletion job in process
+    DeleteJob *mDeleteJob;
+};
+
+#endif
diff --git a/resources/pop3/popsettings.ui b/resources/pop3/popsettings.ui
new file mode 100644 (file)
index 0000000..1aeacd8
--- /dev/null
@@ -0,0 +1,625 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PopPage</class>
+ <widget class="QWidget" name="PopPage">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>451</width>
+    <height>636</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QLabel" name="titleLabel">
+     <property name="font">
+      <font>
+       <weight>75</weight>
+       <bold>true</bold>
+      </font>
+     </property>
+     <property name="text">
+      <string>Account Type: POP Account</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="KSeparator" name="kseparator"/>
+   </item>
+   <item>
+    <widget class="QTabWidget" name="tabWidget">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="currentIndex">
+      <number>0</number>
+     </property>
+     <property name="movable">
+      <bool>false</bool>
+     </property>
+     <widget class="QWidget" name="page1">
+      <attribute name="title">
+       <string>General</string>
+      </attribute>
+      <layout class="QGridLayout" name="gridLayout_3">
+       <item row="2" column="0">
+        <spacer name="verticalSpacer_2">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>20</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+       <item row="1" column="0">
+        <widget class="QGroupBox" name="groupBox_4">
+         <property name="title">
+          <string>Mail Checking Options</string>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_6">
+          <item>
+           <widget class="QCheckBox" name="intervalCheck">
+            <property name="toolTip">
+             <string>If active, the POP3 account will be checked for new mail every x minutes</string>
+            </property>
+            <property name="text">
+             <string>Enable &amp;interval mail checking</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <layout class="QHBoxLayout" name="horizontalLayout">
+            <item>
+             <widget class="QLabel" name="intervalLabel">
+              <property name="text">
+               <string>Check mail interval:</string>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="KPluralHandlingSpinBox" name="intervalSpin">
+              <property name="minimum">
+               <number>1</number>
+              </property>
+              <property name="maximum">
+               <number>10000000</number>
+              </property>
+              <property name="value">
+               <number>5</number>
+              </property>
+             </widget>
+            </item>
+           </layout>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item row="0" column="0">
+        <widget class="QGroupBox" name="groupBox_3">
+         <property name="title">
+          <string>Account Information</string>
+         </property>
+         <layout class="QFormLayout" name="formLayout">
+          <property name="fieldGrowthPolicy">
+           <enum>QFormLayout::ExpandingFieldsGrow</enum>
+          </property>
+          <item row="0" column="0">
+           <widget class="QLabel" name="label_2">
+            <property name="whatsThis">
+             <string>Your Internet Service Provider gave you a &lt;em&gt;user name&lt;/em&gt; which is used to authenticate you with their servers. It usually is the first part of your email address (the part before &lt;em&gt;@&lt;/em&gt;).</string>
+            </property>
+            <property name="text">
+             <string>Account &amp;name:</string>
+            </property>
+            <property name="buddy">
+             <cstring>nameEdit</cstring>
+            </property>
+           </widget>
+          </item>
+          <item row="0" column="1">
+           <widget class="KLineEdit" name="nameEdit">
+            <property name="toolTip">
+             <string>Name displayed in the list of accounts</string>
+            </property>
+            <property name="whatsThis">
+             <string>Account name: This defines the name displayed in the account list.</string>
+            </property>
+            <property name="trapReturnKey" stdset="0">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+          <item row="1" column="0">
+           <widget class="QLabel" name="label_5">
+            <property name="text">
+             <string>Incoming &amp;mail server:</string>
+            </property>
+            <property name="buddy">
+             <cstring>hostEdit</cstring>
+            </property>
+           </widget>
+          </item>
+          <item row="1" column="1">
+           <widget class="KLineEdit" name="hostEdit">
+            <property name="toolTip">
+             <string>Address of the mail POP3 server</string>
+            </property>
+            <property name="whatsThis">
+             <string>The address of the POP3 server, e.g. pop3.yourprovider.org. You should get this address from your mail provider.</string>
+            </property>
+            <property name="trapReturnKey" stdset="0">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+          <item row="2" column="0">
+           <widget class="QLabel" name="label_3">
+            <property name="text">
+             <string>Use&amp;rname:</string>
+            </property>
+            <property name="buddy">
+             <cstring>loginEdit</cstring>
+            </property>
+           </widget>
+          </item>
+          <item row="2" column="1">
+           <widget class="KLineEdit" name="loginEdit">
+            <property name="toolTip">
+             <string>The username that identifies you against the mail server</string>
+            </property>
+            <property name="whatsThis">
+             <string>Your Internet Service Provider gave you a &lt;em&gt;user name&lt;/em&gt; which is used to authenticate you with their servers. It usually is the first part of your email address (the part before &lt;em&gt;@&lt;/em&gt;).</string>
+            </property>
+            <property name="trapReturnKey" stdset="0">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+          <item row="3" column="0">
+           <widget class="QLabel" name="passwordLabel">
+            <property name="text">
+             <string>P&amp;assword:</string>
+            </property>
+            <property name="buddy">
+             <cstring>passwordEdit</cstring>
+            </property>
+           </widget>
+          </item>
+          <item row="3" column="1">
+           <widget class="KLineEdit" name="passwordEdit">
+            <property name="toolTip">
+             <string>Password for access to the mail server</string>
+            </property>
+            <property name="whatsThis">
+             <string>Password: The password given to you by your mail provider.</string>
+            </property>
+            <property name="echoMode">
+             <enum>QLineEdit::Password</enum>
+            </property>
+            <property name="trapReturnKey" stdset="0">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="page2">
+      <attribute name="title">
+       <string>Advanced</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_2">
+       <item>
+        <widget class="QGroupBox" name="groupBox_5">
+         <property name="title">
+          <string>POP Settings</string>
+         </property>
+         <layout class="QGridLayout" name="gridLayout_2">
+          <item row="0" column="0">
+           <widget class="QCheckBox" name="leaveOnServerCheck">
+            <property name="toolTip">
+             <string>If checked the message is not deleted from the mail server</string>
+            </property>
+            <property name="whatsThis">
+             <string>Check this option if you want to fetch only copies of the mails and leave the original mails on the server.</string>
+            </property>
+            <property name="text">
+             <string>Lea&amp;ve fetched messages on the server</string>
+            </property>
+           </widget>
+          </item>
+          <item row="1" column="0">
+           <widget class="QCheckBox" name="leaveOnServerDaysCheck">
+            <property name="sizePolicy">
+             <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+              <horstretch>0</horstretch>
+              <verstretch>0</verstretch>
+             </sizepolicy>
+            </property>
+            <property name="toolTip">
+             <string>The original message is deleted from the server after x days</string>
+            </property>
+            <property name="text">
+             <string>Days to leave messages on the server:</string>
+            </property>
+           </widget>
+          </item>
+          <item row="1" column="1">
+           <widget class="QSpinBox" name="leaveOnServerDaysSpin">
+            <property name="enabled">
+             <bool>false</bool>
+            </property>
+            <property name="minimum">
+             <number>0</number>
+            </property>
+            <property name="maximum">
+             <number>365</number>
+            </property>
+            <property name="value">
+             <number>1</number>
+            </property>
+           </widget>
+          </item>
+          <item row="2" column="0">
+           <widget class="QCheckBox" name="leaveOnServerCountCheck">
+            <property name="toolTip">
+             <string>Only the x most recent messages are kept on the server</string>
+            </property>
+            <property name="whatsThis">
+             <string>Check this option if you want to only keep the x most recent messages on the server and delete all older.</string>
+            </property>
+            <property name="text">
+             <string>Number of messages to keep:</string>
+            </property>
+           </widget>
+          </item>
+          <item row="2" column="1">
+           <widget class="QSpinBox" name="leaveOnServerCountSpin">
+            <property name="enabled">
+             <bool>false</bool>
+            </property>
+            <property name="minimum">
+             <number>0</number>
+            </property>
+            <property name="maximum">
+             <number>999999</number>
+            </property>
+            <property name="value">
+             <number>99</number>
+            </property>
+           </widget>
+          </item>
+          <item row="3" column="0">
+           <widget class="QCheckBox" name="leaveOnServerSizeCheck">
+            <property name="toolTip">
+             <string>Keep most recent messages within the quota and delete oldest</string>
+            </property>
+            <property name="whatsThis">
+             <string>If active, most recent messages are kept until the quota is reached and oldest messages are deleted.</string>
+            </property>
+            <property name="text">
+             <string>Maximum megabytes to keep:</string>
+            </property>
+           </widget>
+          </item>
+          <item row="3" column="1">
+           <widget class="QSpinBox" name="leaveOnServerSizeSpin">
+            <property name="enabled">
+             <bool>false</bool>
+            </property>
+            <property name="suffix">
+             <string> MB</string>
+            </property>
+            <property name="minimum">
+             <number>1</number>
+            </property>
+            <property name="maximum">
+             <number>999999</number>
+            </property>
+            <property name="value">
+             <number>10</number>
+            </property>
+           </widget>
+          </item>
+          <item row="4" column="0">
+           <widget class="QCheckBox" name="filterOnServerCheck">
+            <property name="whatsThis">
+             <string>If you select this option, POP Filters will be used to decide what to do with messages. You can then select to download, delete or keep them on the server.</string>
+            </property>
+            <property name="text">
+             <string>&amp;Filter messages larger than:</string>
+            </property>
+           </widget>
+          </item>
+          <item row="4" column="1">
+           <widget class="QSpinBox" name="filterOnServerSizeSpin">
+            <property name="enabled">
+             <bool>false</bool>
+            </property>
+            <property name="whatsThis">
+             <string>If you select this option, POP Filters will be used to decide what to do with messages. You can then select to download, delete or keep them on the server.</string>
+            </property>
+            <property name="minimum">
+             <number>1</number>
+            </property>
+            <property name="maximum">
+             <number>10000000</number>
+            </property>
+            <property name="value">
+             <number>99</number>
+            </property>
+           </widget>
+          </item>
+          <item row="5" column="0">
+           <widget class="QCheckBox" name="usePipeliningCheck">
+            <property name="text">
+             <string>&amp;Use pipelining for faster mail download</string>
+            </property>
+           </widget>
+          </item>
+          <item row="6" column="0" colspan="2">
+           <layout class="QGridLayout" name="gridLayout">
+            <property name="sizeConstraint">
+             <enum>QLayout::SetMinimumSize</enum>
+            </property>
+            <property name="spacing">
+             <number>4</number>
+            </property>
+            <item row="0" column="0">
+             <widget class="QLabel" name="label_8">
+              <property name="text">
+               <string>Destination folder:</string>
+              </property>
+             </widget>
+            </item>
+            <item row="1" column="0">
+             <widget class="QLabel" name="preCommandLabel">
+              <property name="text">
+               <string>Pre-com&amp;mand:</string>
+              </property>
+              <property name="buddy">
+               <cstring>precommand</cstring>
+              </property>
+             </widget>
+            </item>
+            <item row="1" column="1">
+             <widget class="KLineEdit" name="precommand">
+              <property name="sizePolicy">
+               <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+                <horstretch>0</horstretch>
+                <verstretch>0</verstretch>
+               </sizepolicy>
+              </property>
+              <property name="toolTip">
+               <string>Command that is executed before checking mail</string>
+              </property>
+              <property name="showClearButton" stdset="0">
+               <bool>true</bool>
+              </property>
+              <property name="trapReturnKey" stdset="0">
+               <bool>true</bool>
+              </property>
+             </widget>
+            </item>
+            <item row="0" column="1">
+             <widget class="Akonadi::CollectionRequester" name="folderRequester"/>
+            </item>
+           </layout>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <widget class="QGroupBox" name="safeImap">
+         <property name="whatsThis">
+          <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'HandelGotDLig'; font-size:10pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Sans Serif'; font-weight:600;&quot;&gt;SSL/TLS&lt;/span&gt;&lt;span style=&quot; font-family:'Sans Serif';&quot;&gt; is safe IMAP over port 993;&lt;/span&gt;&lt;/p&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Sans Serif'; font-weight:600;&quot;&gt;STARTTLS&lt;/span&gt;&lt;span style=&quot; font-family:'Sans Serif';&quot;&gt; will operate on port 143 and switch to a secure connection directly after connecting;&lt;/span&gt;&lt;/p&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Sans Serif'; font-weight:600;&quot;&gt;None&lt;/span&gt;&lt;span style=&quot; font-family:'Sans Serif';&quot;&gt; will connect to port 143 but not switch to a secure connection. This setting is not recommended.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+         </property>
+         <property name="title">
+          <string>Connection Settings</string>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_8">
+          <item>
+           <widget class="QStackedWidget" name="checkCapabilitiesStack">
+            <property name="currentIndex">
+             <number>0</number>
+            </property>
+            <widget class="QWidget" name="page">
+             <layout class="QVBoxLayout" name="verticalLayout_3">
+              <item>
+               <widget class="QPushButton" name="checkCapabilities">
+                <property name="text">
+                 <string>Auto Detect</string>
+                </property>
+               </widget>
+              </item>
+             </layout>
+            </widget>
+            <widget class="QWidget" name="page_2">
+             <layout class="QVBoxLayout" name="verticalLayout_4">
+              <item>
+               <widget class="QProgressBar" name="checkCapabilitiesProgress">
+                <property name="value">
+                 <number>0</number>
+                </property>
+               </widget>
+              </item>
+             </layout>
+            </widget>
+           </widget>
+          </item>
+          <item>
+           <layout class="QFormLayout" name="formLayout_3">
+            <property name="fieldGrowthPolicy">
+             <enum>QFormLayout::ExpandingFieldsGrow</enum>
+            </property>
+            <item row="1" column="0">
+             <widget class="QLabel" name="label_11">
+              <property name="text">
+               <string>Encryption:</string>
+              </property>
+             </widget>
+            </item>
+            <item row="1" column="1">
+             <layout class="QHBoxLayout" name="horizontalLayout_2">
+              <item>
+               <widget class="QRadioButton" name="encryptionNone">
+                <property name="text">
+                 <string>None</string>
+                </property>
+                <property name="checked">
+                 <bool>true</bool>
+                </property>
+               </widget>
+              </item>
+              <item>
+               <widget class="QRadioButton" name="encryptionSSL">
+                <property name="text">
+                 <string>SSL/TLS</string>
+                </property>
+               </widget>
+              </item>
+              <item>
+               <widget class="QRadioButton" name="encryptionTLS">
+                <property name="text">
+                 <string>STA&amp;RTTLS</string>
+                </property>
+               </widget>
+              </item>
+             </layout>
+            </item>
+            <item row="3" column="0">
+             <widget class="QLabel" name="label_12">
+              <property name="text">
+               <string>Authentication:</string>
+              </property>
+             </widget>
+            </item>
+            <item row="3" column="1">
+             <widget class="QComboBox" name="authCombo"/>
+            </item>
+            <item row="2" column="0">
+             <widget class="QLabel" name="label_13">
+              <property name="text">
+               <string>Port:</string>
+              </property>
+             </widget>
+            </item>
+            <item row="2" column="1">
+             <widget class="QSpinBox" name="portEdit">
+              <property name="minimum">
+               <number>1</number>
+              </property>
+              <property name="maximum">
+               <number>65534</number>
+              </property>
+              <property name="value">
+               <number>110</number>
+              </property>
+             </widget>
+            </item>
+           </layout>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <spacer name="verticalSpacer_3">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>20</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KSeparator</class>
+   <extends>QFrame</extends>
+   <header>kseparator.h</header>
+   <container>1</container>
+  </customwidget>
+  <customwidget>
+   <class>KLineEdit</class>
+   <extends>QLineEdit</extends>
+   <header>klineedit.h</header>
+   <container>1</container>
+  </customwidget>
+  <customwidget>
+   <class>KPluralHandlingSpinBox</class>
+   <extends>QSpinBox</extends>
+   <header>kpluralhandlingspinbox.h</header>
+  </customwidget>
+  <customwidget>
+   <class>Akonadi::CollectionRequester</class>
+   <extends>QFrame</extends>
+   <header>CollectionRequester</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <tabstops>
+  <tabstop>tabWidget</tabstop>
+  <tabstop>nameEdit</tabstop>
+  <tabstop>hostEdit</tabstop>
+  <tabstop>loginEdit</tabstop>
+  <tabstop>passwordEdit</tabstop>
+  <tabstop>intervalCheck</tabstop>
+  <tabstop>intervalSpin</tabstop>
+  <tabstop>leaveOnServerCheck</tabstop>
+  <tabstop>leaveOnServerDaysCheck</tabstop>
+  <tabstop>leaveOnServerDaysSpin</tabstop>
+  <tabstop>leaveOnServerCountCheck</tabstop>
+  <tabstop>leaveOnServerCountSpin</tabstop>
+  <tabstop>leaveOnServerSizeCheck</tabstop>
+  <tabstop>leaveOnServerSizeSpin</tabstop>
+  <tabstop>filterOnServerCheck</tabstop>
+  <tabstop>filterOnServerSizeSpin</tabstop>
+  <tabstop>usePipeliningCheck</tabstop>
+  <tabstop>precommand</tabstop>
+  <tabstop>checkCapabilities</tabstop>
+  <tabstop>encryptionNone</tabstop>
+  <tabstop>encryptionSSL</tabstop>
+  <tabstop>encryptionTLS</tabstop>
+  <tabstop>portEdit</tabstop>
+  <tabstop>authCombo</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/resources/pop3/settings.cpp b/resources/pop3/settings.cpp
new file mode 100644 (file)
index 0000000..b7ca260
--- /dev/null
@@ -0,0 +1,84 @@
+/* Copyright 2010 Thomas McGuire <mcguire@kde.org>
+
+   This library is free software; you can redistribute it and/or modify
+   it under the terms of the GNU Library General Public License as published
+   by the Free Software Foundation; either version 2 of the License or
+   ( at your option ) version 3 or, at the discretion of KDE e.V.
+   ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version.
+
+   This library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Library General Public License for more details.
+
+   You should have received a copy of the GNU Library General Public License
+   along with this library; see the file COPYING.LIB.  If not, write to
+   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+   Boston, MA 02110-1301, USA.
+*/
+#include "settings.h"
+#include "settingsadaptor.h"
+
+#include <kwallet.h>
+#include "pop3resource_debug.h"
+
+class SettingsHelper
+{
+public:
+    SettingsHelper() : q(Q_NULLPTR) {}
+    ~SettingsHelper()
+    {
+        qCWarning(POP3RESOURCE_LOG) << q;
+        delete q;
+        q = Q_NULLPTR;
+    }
+    Settings *q;
+};
+
+Q_GLOBAL_STATIC(SettingsHelper, s_globalSettings)
+
+Settings *Settings::self()
+{
+    if (!s_globalSettings->q) {
+        new Settings;
+        s_globalSettings->q->load();
+    }
+    return s_globalSettings->q;
+}
+
+Settings::Settings()
+{
+    Q_ASSERT(!s_globalSettings->q);
+    s_globalSettings->q = this;
+
+    new SettingsAdaptor(this);
+    QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"), this,
+            QDBusConnection::ExportAdaptors | QDBusConnection::ExportScriptableContents);
+}
+
+void Settings::setWindowId(WId id)
+{
+    mWinId = id;
+}
+
+void Settings::setResourceId(const QString &resourceIdentifier)
+{
+    mResourceId = resourceIdentifier;
+}
+
+void Settings::setPassword(const QString &password)
+{
+    using namespace KWallet;
+    Wallet *wallet = Wallet::openWallet(Wallet::NetworkWallet(), mWinId,
+                                        Wallet::Synchronous);
+    if (wallet && wallet->isOpen()) {
+        if (!wallet->hasFolder(QStringLiteral("pop3"))) {
+            wallet->createFolder(QStringLiteral("pop3"));
+        }
+        wallet->setFolder(QStringLiteral("pop3"));
+        wallet->writePassword(mResourceId, password);
+    } else {
+        qCWarning(POP3RESOURCE_LOG) << "Unable to open wallet!";
+    }
+    delete wallet;
+}
diff --git a/resources/pop3/settings.h b/resources/pop3/settings.h
new file mode 100644 (file)
index 0000000..77f4469
--- /dev/null
@@ -0,0 +1,47 @@
+/* Copyright 2010 Thomas McGuire <mcguire@kde.org>
+
+   This library is free software; you can redistribute it and/or modify
+   it under the terms of the GNU Library General Public License as published
+   by the Free Software Foundation; either version 2 of the License or
+   ( at your option ) version 3 or, at the discretion of KDE e.V.
+   ( which shall act as a proxy as in section 14 of the GPLv3 ), any later version.
+
+   This library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Library General Public License for more details.
+
+   You should have received a copy of the GNU Library General Public License
+   along with this library; see the file COPYING.LIB.  If not, write to
+   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+   Boston, MA 02110-1301, USA.
+*/
+#ifndef SETTINGS_H
+#define SETTINGS_H
+
+#include "settingsbase.h"
+
+#include <qwindowdefs.h>
+
+/**
+ * Extended settings class that allows setting the password over dbus, which is used by the
+ * wizard.
+ */
+class Settings : public SettingsBase
+{
+    Q_OBJECT
+    Q_CLASSINFO("D-Bus Interface", "org.kde.Akonadi.POP3.Wallet")
+public:
+    Settings();
+    void setWindowId(WId id);
+    void setResourceId(const QString &resourceIdentifier);
+    static Settings *self();
+
+public Q_SLOTS:
+    Q_SCRIPTABLE void setPassword(const QString &password);
+private:
+    WId mWinId;
+    QString mResourceId;
+};
+
+#endif
diff --git a/resources/pop3/settings.kcfg b/resources/pop3/settings.kcfg
new file mode 100644 (file)
index 0000000..6cff786
--- /dev/null
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:kcfg="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
+      http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
+  <kcfgfile/>
+  <group name="General">
+    <entry name="unitTestPassword" type="String">
+      <label></label>
+      <default></default>
+    </entry>
+    <entry name="login" type="String">
+      <label></label>
+      <default></default>
+    </entry>
+    <entry name="host" type="String">
+      <label></label>
+      <default></default>
+    </entry>
+    <entry name="port" type="UInt">
+      <label></label>
+      <default>110</default>
+    </entry>
+    <entry name="authenticationMethod" type="Int">
+      <label>Defines the authentication type to use</label>
+      <default>7</default>
+    </entry>
+    <entry name="useSSL" type="Bool">
+      <label></label>
+      <default>false</default>
+    </entry>
+    <entry name="useTLS" type="Bool">
+      <label></label>
+      <default>false</default>
+    </entry>
+    <entry name="pipelining" type="Bool">
+      <label></label>
+      <default>false</default>
+    </entry>
+    <entry name="leaveOnServer" type="Bool">
+      <label></label>
+      <default>false</default>
+    </entry>
+    <entry name="leaveOnServerDays" type="Int">
+      <label></label>
+      <default>-1</default>
+    </entry>
+    <entry name="leaveOnServerCount" type="Int">
+      <label></label>
+      <default>-1</default>
+    </entry>
+    <entry name="leaveOnServerSize" type="Int">
+      <label></label>
+      <default>-1</default>
+    </entry>
+    <entry name="filterOnServer" type="Bool">
+      <label></label>
+      <default>false</default>
+    </entry>
+        <entry name="filterCheckSize" type="UInt">
+      <label></label>
+      <default>50000</default>
+    </entry>
+    <entry name="targetCollection" type="LongLong">
+      <label></label>
+      <default>-1</default>
+    </entry>
+    <entry name="precommand" type="String">
+       <default></default>
+     </entry>
+     <entry name="intervalCheckEnabled" type="Bool">
+       <default>false</default>
+     </entry>
+     <entry name="intervalCheckInterval" type="Int">
+        <default>5</default>
+     </entry>
+  </group>
+  <group name="LeaveOnServer">
+    <entry name="seenUidList" type="StringList">
+      <label></label>
+      <default></default>
+    </entry>
+    <entry name="seenUidTimeList" type="IntList">
+      <label></label>
+      <default></default>
+    </entry>
+    <entry name="downloadLater" type="StringList">
+      <label></label>
+      <default></default>
+    </entry>      
+  </group>
+</kcfg>
diff --git a/resources/pop3/settingsbase.kcfgc b/resources/pop3/settingsbase.kcfgc
new file mode 100644 (file)
index 0000000..2f8e8d5
--- /dev/null
@@ -0,0 +1,8 @@
+File=settings.kcfg
+ClassName=SettingsBase
+Mutators=true
+ItemAccessors=true
+SetUserTexts=true
+Singleton=false
+IncludeFiles=metatype.h
+GlobalEnums=true
diff --git a/resources/pop3/wizard/CMakeLists.txt b/resources/pop3/wizard/CMakeLists.txt
new file mode 100644 (file)
index 0000000..f59a173
--- /dev/null
@@ -0,0 +1,2 @@
+
+install ( FILES pop3wizard.desktop pop3wizard.es pop3wizard.ui DESTINATION ${KDE_INSTALL_DATADIR}/akonadi/accountwizard/pop3 )
diff --git a/resources/pop3/wizard/Messages.sh b/resources/pop3/wizard/Messages.sh
new file mode 100644 (file)
index 0000000..2689f1f
--- /dev/null
@@ -0,0 +1,4 @@
+#! /usr/bin/env bash
+$EXTRACTRC *.ui >> rc.cpp
+$XGETTEXT *.cpp -o $podir/accountwizard_pop3.pot
+$XGETTEXT -kqsTr *.es -j -o $podir/accountwizard_pop3.pot
diff --git a/resources/pop3/wizard/pop3wizard.desktop b/resources/pop3/wizard/pop3wizard.desktop
new file mode 100644 (file)
index 0000000..616aaf3
--- /dev/null
@@ -0,0 +1,97 @@
+[Desktop Entry]
+Name=POP3
+Name[ast]=POP3
+Name[bg]=POP3
+Name[ca]=POP3
+Name[ca@valencia]=POP3
+Name[cs]=POP3
+Name[da]=POP3
+Name[de]=POP3
+Name[el]=POP3
+Name[en_GB]=POP3
+Name[es]=POP3
+Name[et]=POP3
+Name[fi]=POP3
+Name[fr]=POP3
+Name[gl]=POP3
+Name[hu]=POP3
+Name[it]=POP3
+Name[ko]=POP3
+Name[lv]=POP3
+Name[nb]=POP3
+Name[nds]=POP3
+Name[nl]=POP3
+Name[nn]=POP3
+Name[pa]=POP3
+Name[pl]=POP3
+Name[pt]=POP3
+Name[pt_BR]=POP3
+Name[ru]=POP3
+Name[sk]=POP3
+Name[sl]=POP3
+Name[sr]=ПОП3
+Name[sr@ijekavian]=ПОП3
+Name[sr@ijekavianlatin]=POP3
+Name[sr@latin]=POP3
+Name[sv]=Pop3
+Name[tr]=POP3
+Name[uk]=POP3
+Name[x-test]=xxPOP3xx
+Name[zh_CN]=POP3
+Name[zh_TW]=POP3
+Icon=message-rfc822
+Comment=Pop3 account
+Comment[bg]=Сметка POP3
+Comment[bs]=Pop3 nalog
+Comment[ca]=Compte Pop3
+Comment[ca@valencia]=Compte Pop3
+Comment[cs]=Pop3 účet
+Comment[da]=POP3-konto
+Comment[de]=POP3-Postfach
+Comment[el]=Λογαριασμός pop3
+Comment[en_GB]=Pop3 account
+Comment[es]=Cuenta Pop3
+Comment[et]=POP3 konto
+Comment[fi]=POP3-tili
+Comment[fr]=Compte POP3
+Comment[ga]=Cuntas POP3
+Comment[gl]=Conta de Pop3
+Comment[hr]=Pop3 račun
+Comment[hu]=POP3 azonosító
+Comment[ia]=Conto POP3
+Comment[it]=Account POP3
+Comment[ja]=Pop 3 アカウント
+Comment[kk]=Pop3 тіркелгісі
+Comment[km]=គនណី Pop3
+Comment[ko]=POP3 계정
+Comment[lt]=Pop3 paskyra
+Comment[lv]=Pop3 konts
+Comment[nb]=Pop3 konto
+Comment[nds]=POP3-Konto
+Comment[nl]=POP3-account
+Comment[nn]=POP3-konto
+Comment[pa]=Pop੩ ਅਕਾਊਂਟ
+Comment[pl]=Konto Pop3
+Comment[pt]=Conta de POP3
+Comment[pt_BR]=Conta POP3
+Comment[ro]=Cont Pop3
+Comment[ru]=Учётная запись POP3
+Comment[sk]=Pop3 účet
+Comment[sl]=Račun POP3
+Comment[sr]=ПОП3 налог
+Comment[sr@ijekavian]=ПОП3 налог
+Comment[sr@ijekavianlatin]=POP3 nalog
+Comment[sr@latin]=POP3 nalog
+Comment[sv]=Pop3-konto
+Comment[tr]=Pop3 hesabı
+Comment[uk]=Обліковий запис POP3
+Comment[x-test]=xxPop3 accountxx
+Comment[zh_CN]=Pop3 账户
+Comment[zh_TW]=Pop3 帳號
+
+[Wizard]
+Type=message/rfc822
+Script=pop3wizard.es
+
+[Translate]
+Filename=accountwizard_pop3
diff --git a/resources/pop3/wizard/pop3wizard.es b/resources/pop3/wizard/pop3wizard.es
new file mode 100644 (file)
index 0000000..81d511d
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+    Copyright (c) 2009 Montel Laurent <montel@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+// add this function to trim user input of whitespace when needed
+String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ""); };
+
+var page = Dialog.addPage( "pop3wizard.ui", qsTr("Personal Settings") );
+
+var userChangedServerAddress = false;
+
+function serverChanged( arg )
+{
+  validateInput();
+  if ( arg == "" ) {
+    userChangedServerAddress = false;
+  } else {
+    userChangedServerAddress = true;
+  }
+}
+
+function validateInput()
+{
+  if ( page.widget().incommingAddress.text.trim() == "" ) {
+    page.setValid( false );
+  } else {
+    page.setValid( true );
+  }
+}
+
+function setup()
+{
+  var pop3Res = SetupManager.createResource( "akonadi_pop3_resource" );
+  pop3Res.setOption( "Host", page.widget().incommingAddress.text.trim() );
+  pop3Res.setOption( "Login", page.widget().userName.text.trim() );
+  pop3Res.setOption( "Password", SetupManager.password() );
+
+  var smtp = SetupManager.createTransport( "smtp" );
+  smtp.setName( SetupManager.name() );
+  smtp.setHost( page.widget().outgoingAddress.text.trim() );
+  smtp.setEncryption( "NONE" );
+
+  SetupManager.execute();
+}
+
+page.widget().incommingAddress.textChanged.connect( serverChanged );
+page.pageLeftNext.connect( setup );
+validateInput();
diff --git a/resources/pop3/wizard/pop3wizard.ui b/resources/pop3/wizard/pop3wizard.ui
new file mode 100644 (file)
index 0000000..4f623ea
--- /dev/null
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>pop3Wizard</class>
+ <widget class="QWidget" name="pop3Wizard">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>131</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_3">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <layout class="QVBoxLayout" name="verticalLayout_2">
+       <item>
+        <widget class="QLabel" name="label">
+         <property name="text">
+          <string>Username:</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLabel" name="label_2">
+         <property name="text">
+          <string>Incoming server:</string>
+         </property>
+         <property name="buddy">
+          <cstring>incommingAddress</cstring>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLabel" name="label_4">
+         <property name="text">
+          <string>Outgoing server:</string>
+         </property>
+         <property name="buddy">
+          <cstring>outgoingAddress</cstring>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </item>
+     <item>
+      <layout class="QVBoxLayout" name="verticalLayout">
+       <item>
+        <widget class="KLineEdit" name="userName"/>
+       </item>
+       <item>
+        <widget class="KLineEdit" name="incommingAddress"/>
+       </item>
+       <item>
+        <widget class="KLineEdit" name="outgoingAddress">
+         <property name="text">
+          <string/>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <spacer name="verticalSpacer_2">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>138</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KLineEdit</class>
+   <extends>QLineEdit</extends>
+   <header>klineedit.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/resources/shared/CMakeLists.txt b/resources/shared/CMakeLists.txt
new file mode 100644 (file)
index 0000000..66d6bf3
--- /dev/null
@@ -0,0 +1,3 @@
+add_subdirectory(filestore)
+add_subdirectory(singlefileresource)
+
diff --git a/resources/shared/filestore/CMakeLists.txt b/resources/shared/filestore/CMakeLists.txt
new file mode 100644 (file)
index 0000000..852b7ef
--- /dev/null
@@ -0,0 +1,42 @@
+project(akonadi-filestore)
+
+
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi-filestore\")
+
+set(akonadi-filestore_SRCS
+  abstractlocalstore.cpp
+  collectioncreatejob.cpp
+  collectiondeletejob.cpp
+  collectionfetchjob.cpp
+  collectionmodifyjob.cpp
+  collectionmovejob.cpp
+  entitycompactchangeattribute.cpp
+  itemcreatejob.cpp
+  itemdeletejob.cpp
+  itemfetchjob.cpp
+  itemmodifyjob.cpp
+  itemmovejob.cpp
+  job.cpp
+  session.cpp
+  sessionimpls.cpp
+  storecompactjob.cpp
+)
+
+add_library(akonadi-filestore  ${akonadi-filestore_SRCS} )
+generate_export_header(akonadi-filestore BASE_NAME akonadi-filestore)
+
+target_link_libraries(akonadi-filestore
+  PUBLIC
+                      KF5::AkonadiCore
+  PRIVATE
+                      KF5::I18n
+)
+
+set_target_properties(akonadi-filestore PROPERTIES VERSION ${KDEPIMRUNTIME_LIB_VERSION} SOVERSION ${KDEPIMRUNTIME_LIB_SOVERSION} )
+
+install(TARGETS akonadi-filestore ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP)
+
+if (BUILD_TESTING)
+  add_subdirectory( autotests )
+endif ()
+
diff --git a/resources/shared/filestore/Messages.sh b/resources/shared/filestore/Messages.sh
new file mode 100644 (file)
index 0000000..2a8836d
--- /dev/null
@@ -0,0 +1,2 @@
+#! /usr/bin/env bash
+$XGETTEXT *.cpp *.h -o $podir/akonadi-filestore.pot
diff --git a/resources/shared/filestore/abstractlocalstore.cpp b/resources/shared/filestore/abstractlocalstore.cpp
new file mode 100644 (file)
index 0000000..447ef06
--- /dev/null
@@ -0,0 +1,884 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "abstractlocalstore.h"
+
+#include "collectioncreatejob.h"
+#include "collectiondeletejob.h"
+#include "collectionfetchjob.h"
+#include "collectionmodifyjob.h"
+#include "collectionmovejob.h"
+#include "itemcreatejob.h"
+#include "itemdeletejob.h"
+#include "itemfetchjob.h"
+#include "itemmodifyjob.h"
+#include "itemmovejob.h"
+#include "sessionimpls_p.h"
+#include "storecompactjob.h"
+
+#include <collection.h>
+#include <entitydisplayattribute.h>
+
+#include <KLocalizedString>
+#include <QDebug>
+#include <QFileInfo>
+
+using namespace Akonadi;
+
+class JobProcessingAdaptor : public FileStore::Job::Visitor
+{
+public:
+    explicit JobProcessingAdaptor(FileStore::AbstractJobSession *session)
+        : mSession(session)
+    {
+    }
+
+public: // Job::Visitor interface implementation
+    bool visit(FileStore::Job *job) Q_DECL_OVERRIDE {
+        Q_UNUSED(job);
+        return false;
+    }
+
+    bool visit(FileStore::CollectionCreateJob *job) Q_DECL_OVERRIDE {
+        Q_UNUSED(job);
+        return false;
+    }
+
+    bool visit(FileStore::CollectionDeleteJob *job) Q_DECL_OVERRIDE {
+        Q_UNUSED(job);
+        return false;
+    }
+
+    bool visit(FileStore::CollectionFetchJob *job) Q_DECL_OVERRIDE {
+        Q_UNUSED(job);
+        return false;
+    }
+
+    bool visit(FileStore::CollectionModifyJob *job) Q_DECL_OVERRIDE {
+        Q_UNUSED(job);
+        return false;
+    }
+
+    bool visit(FileStore::CollectionMoveJob *job) Q_DECL_OVERRIDE {
+        Q_UNUSED(job);
+        return false;
+    }
+
+    bool visit(FileStore::ItemCreateJob *job) Q_DECL_OVERRIDE {
+        Q_UNUSED(job);
+        return false;
+    }
+
+    bool visit(FileStore::ItemDeleteJob *job) Q_DECL_OVERRIDE {
+        Q_UNUSED(job);
+        return false;
+    }
+
+    bool visit(FileStore::ItemFetchJob *job) Q_DECL_OVERRIDE {
+        Q_UNUSED(job);
+        return false;
+    }
+
+    bool visit(FileStore::ItemModifyJob *job) Q_DECL_OVERRIDE {
+        Q_UNUSED(job);
+        return false;
+    }
+
+    bool visit(FileStore::ItemMoveJob *job) Q_DECL_OVERRIDE {
+        Q_UNUSED(job);
+        return false;
+    }
+
+    bool visit(FileStore::StoreCompactJob *job) Q_DECL_OVERRIDE {
+        Q_UNUSED(job);
+        return false;
+    }
+
+protected:
+    FileStore::AbstractJobSession *mSession;
+};
+
+class TopLevelCollectionFetcher : public JobProcessingAdaptor
+{
+public:
+    explicit TopLevelCollectionFetcher(FileStore::AbstractJobSession *session)
+        : JobProcessingAdaptor(session)
+    {
+    }
+
+    void setTopLevelCollection(const Collection &collection)
+    {
+        mTopLevelCollection = collection;
+    }
+
+public:
+    using JobProcessingAdaptor::visit;
+
+    bool visit(FileStore::CollectionFetchJob *job) Q_DECL_OVERRIDE {
+        if (job->type() == FileStore::CollectionFetchJob::Base &&
+        job->collection().remoteId() == mTopLevelCollection.remoteId())
+        {
+            mSession->notifyCollectionsReceived(job, Collection::List() << mTopLevelCollection);
+            return true;
+        }
+
+        return false;
+    }
+
+private:
+    Collection mTopLevelCollection;
+};
+
+class CollectionsProcessedNotifier : public JobProcessingAdaptor
+{
+public:
+    explicit CollectionsProcessedNotifier(FileStore::AbstractJobSession *session)
+        : JobProcessingAdaptor(session)
+    {
+    }
+
+    void setCollections(const Collection::List &collections)
+    {
+        mCollections = collections;
+    }
+
+public:
+    using JobProcessingAdaptor::visit;
+
+    bool visit(FileStore::CollectionCreateJob *job) Q_DECL_OVERRIDE {
+        Q_ASSERT(!mCollections.isEmpty());
+        if (mCollections.count() > 1)
+        {
+            qCritical() << "Processing collections for CollectionCreateJob "
+            "encountered more than one collection. Just processing the first one.";
+        }
+
+        mSession->notifyCollectionCreated(job, mCollections[ 0 ]);
+        return true;
+    }
+
+    bool visit(FileStore::CollectionDeleteJob *job) Q_DECL_OVERRIDE {
+        Q_ASSERT(!mCollections.isEmpty());
+        if (mCollections.count() > 1)
+        {
+            qCritical() << "Processing collections for CollectionDeleteJob "
+            "encountered more than one collection. Just processing the first one.";
+        }
+
+        mSession->notifyCollectionDeleted(job, mCollections[ 0 ]);
+        return true;
+    }
+
+    bool visit(FileStore::CollectionFetchJob *job) Q_DECL_OVERRIDE {
+        mSession->notifyCollectionsReceived(job, mCollections);
+        return true;
+    }
+
+    bool visit(FileStore::CollectionModifyJob *job) Q_DECL_OVERRIDE {
+        Q_ASSERT(!mCollections.isEmpty());
+        if (mCollections.count() > 1)
+        {
+            qCritical() << "Processing collections for CollectionModifyJob "
+            "encountered more than one collection. Just processing the first one.";
+        }
+
+        mSession->notifyCollectionModified(job, mCollections[ 0 ]);
+        return true;
+    }
+
+    bool visit(FileStore::CollectionMoveJob *job) Q_DECL_OVERRIDE {
+        Q_ASSERT(!mCollections.isEmpty());
+        if (mCollections.count() > 1)
+        {
+            qCritical() << "Processing collections for CollectionMoveJob "
+            "encountered more than one collection. Just processing the first one.";
+        }
+
+        mSession->notifyCollectionMoved(job, mCollections[ 0 ]);
+        return true;
+    }
+
+    bool visit(FileStore::StoreCompactJob *job) Q_DECL_OVERRIDE {
+        mSession->notifyCollectionsChanged(job, mCollections);
+        return true;
+    }
+
+private:
+    Collection::List mCollections;
+};
+
+class ItemsProcessedNotifier : public JobProcessingAdaptor
+{
+public:
+    explicit ItemsProcessedNotifier(FileStore::AbstractJobSession *session)
+        : JobProcessingAdaptor(session)
+    {
+    }
+
+    void setItems(const Item::List &items)
+    {
+        mItems = items;
+    }
+
+    void clearItems()
+    {
+        mItems.clear();
+    }
+
+public:
+    using JobProcessingAdaptor::visit;
+
+    bool visit(FileStore::ItemCreateJob *job) Q_DECL_OVERRIDE {
+        Q_ASSERT(!mItems.isEmpty());
+        if (mItems.count() > 1)
+        {
+            qCritical() << "Processing items for ItemCreateJob encountered more than one item. "
+            "Just processing the first one.";
+        }
+
+        mSession->notifyItemCreated(job, mItems[ 0 ]);
+        return true;
+    }
+
+    bool visit(FileStore::ItemFetchJob *job) Q_DECL_OVERRIDE {
+        mSession->notifyItemsReceived(job, mItems);
+        return true;
+    }
+
+    bool visit(FileStore::ItemModifyJob *job) Q_DECL_OVERRIDE {
+        Q_ASSERT(!mItems.isEmpty());
+        if (mItems.count() > 1)
+        {
+            qCritical() << "Processing items for ItemModifyJob encountered more than one item. "
+            "Just processing the first one.";
+        }
+
+        mSession->notifyItemModified(job, mItems[ 0 ]);
+        return true;
+    }
+
+    bool visit(FileStore::ItemMoveJob *job) Q_DECL_OVERRIDE {
+        Q_ASSERT(!mItems.isEmpty());
+        if (mItems.count() > 1)
+        {
+            qCritical() << "Processing items for ItemMoveJob encountered more than one item. "
+            "Just processing the first one.";
+        }
+
+        mSession->notifyItemMoved(job, mItems[ 0 ]);
+        return true;
+    }
+
+    bool visit(FileStore::StoreCompactJob *job) Q_DECL_OVERRIDE {
+        mSession->notifyItemsChanged(job, mItems);
+        return true;
+    }
+
+private:
+    Item::List mItems;
+};
+
+class FileStore::AbstractLocalStore::Private
+{
+    AbstractLocalStore *const q;
+
+public:
+    explicit Private(FileStore::AbstractLocalStore *parent)
+        : q(parent), mSession(new FileStore::FiFoQueueJobSession(q)), mCurrentJob(Q_NULLPTR),
+          mTopLevelCollectionFetcher(mSession), mCollectionsProcessedNotifier(mSession),
+          mItemsProcessedNotifier(mSession)
+    {
+    }
+
+public:
+    QFileInfo mPathFileInfo;
+    Collection mTopLevelCollection;
+
+    FileStore::AbstractJobSession *mSession;
+    FileStore::Job *mCurrentJob;
+
+    TopLevelCollectionFetcher mTopLevelCollectionFetcher;
+    CollectionsProcessedNotifier mCollectionsProcessedNotifier;
+    ItemsProcessedNotifier mItemsProcessedNotifier;
+
+public:
+    void processJobs(const QList<FileStore::Job *> &jobs);
+};
+
+void FileStore::AbstractLocalStore::Private::processJobs(const QList<FileStore::Job *> &jobs)
+{
+    Q_FOREACH (FileStore::Job *job, jobs) {
+        mCurrentJob = job;
+
+        if (job->error() == 0) {
+            if (!job->accept(&mTopLevelCollectionFetcher)) {
+                q->processJob(job);
+            }
+        }
+        mSession->emitResult(job);
+        mCurrentJob = Q_NULLPTR;
+    }
+}
+
+FileStore::AbstractLocalStore::AbstractLocalStore()
+    : QObject(), d(new Private(this))
+{
+    connect(d->mSession, SIGNAL(jobsReady(QList<FileStore::Job*>)), this, SLOT(processJobs(QList<FileStore::Job*>)));
+}
+
+FileStore::AbstractLocalStore::~AbstractLocalStore()
+{
+    delete d;
+}
+
+void FileStore::AbstractLocalStore::setPath(const QString &path)
+{
+    QFileInfo pathFileInfo(path);
+    if (pathFileInfo.fileName().isEmpty()) {
+        pathFileInfo = QFileInfo(pathFileInfo.path());
+    }
+    pathFileInfo.makeAbsolute();
+
+    if (pathFileInfo.absoluteFilePath() == d->mPathFileInfo.absoluteFilePath()) {
+        return;
+    }
+
+    d->mPathFileInfo = pathFileInfo;
+
+    Collection collection;
+    collection.setRemoteId(d->mPathFileInfo.absoluteFilePath());
+    collection.setName(d->mPathFileInfo.fileName());
+
+    EntityDisplayAttribute *attribute = collection.attribute<EntityDisplayAttribute>();
+    if (attribute) {
+        attribute->setDisplayName(d->mPathFileInfo.fileName());
+    }
+
+    setTopLevelCollection(collection);
+}
+
+QString FileStore::AbstractLocalStore::path() const
+{
+    return d->mPathFileInfo.absoluteFilePath();
+}
+
+Collection FileStore::AbstractLocalStore::topLevelCollection() const
+{
+    return d->mTopLevelCollection;
+}
+
+FileStore::CollectionCreateJob *FileStore::AbstractLocalStore::createCollection(const Collection &collection, const Collection &targetParent)
+{
+    FileStore::CollectionCreateJob *job = new FileStore::CollectionCreateJob(collection, targetParent, d->mSession);
+
+    if (d->mTopLevelCollection.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Configured storage location is empty");
+        qCritical() << message;
+        qCritical() << collection << targetParent;
+        d->mSession->setError(job, FileStore::Job::InvalidStoreState, message);
+    } else if (targetParent.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Given folder name is empty");
+        qCritical() << message;
+        qCritical() << collection << targetParent;
+        d->mSession->setError(job, FileStore::Job::InvalidJobContext, message);
+    } else if ((targetParent.rights() & Collection::CanCreateCollection) == 0) {
+        const QString message = i18nc("@info:status", "Access control prohibits folder creation in folder %1", targetParent.name());
+        qCritical() << message;
+        qCritical() << collection << targetParent;
+        d->mSession->setError(job, FileStore::Job::InvalidJobContext, message);
+    }
+
+    int errorCode = 0;
+    QString errorText;
+    checkCollectionCreate(job, errorCode, errorText);
+    if (errorCode != 0) {
+        d->mSession->setError(job, errorCode, errorText);
+    }
+
+    return job;
+}
+
+FileStore::CollectionDeleteJob *FileStore::AbstractLocalStore::deleteCollection(const Collection &collection)
+{
+    FileStore::CollectionDeleteJob *job = new FileStore::CollectionDeleteJob(collection, d->mSession);
+
+    if (d->mTopLevelCollection.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Configured storage location is empty");
+        qCritical() << message;
+        qCritical() << collection;
+        d->mSession->setError(job, FileStore::Job::InvalidStoreState, message);
+    } else if (collection.remoteId().isEmpty() ||
+               collection.parentCollection().remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Given folder name is empty");
+        qCritical() << message;
+        qCritical() << collection;
+        d->mSession->setError(job, FileStore::Job::InvalidJobContext, message);
+    } else if ((collection.rights() & Collection::CanDeleteCollection) == 0) {
+        const QString message = i18nc("@info:status", "Access control prohibits folder deletion in folder %1", collection.name());
+        qCritical() << message;
+        qCritical() << collection;
+        d->mSession->setError(job, FileStore::Job::InvalidJobContext, message);
+    }
+
+    int errorCode = 0;
+    QString errorText;
+    checkCollectionDelete(job, errorCode, errorText);
+    if (errorCode != 0) {
+        d->mSession->setError(job, errorCode, errorText);
+    }
+
+    return job;
+}
+
+FileStore::CollectionFetchJob *FileStore::AbstractLocalStore::fetchCollections(const Collection &collection, FileStore::CollectionFetchJob::Type type) const
+{
+    FileStore::CollectionFetchJob *job = new FileStore::CollectionFetchJob(collection, type, d->mSession);
+
+    if (d->mTopLevelCollection.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Configured storage location is empty");
+        qCritical() << message;
+        qCritical() << collection << "FetchType=" << type;
+        d->mSession->setError(job, FileStore::Job::InvalidStoreState, message);
+    } else if (collection.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Given folder name is empty");
+        qCritical() << message;
+        qCritical() << collection << "FetchType=" << type;
+        d->mSession->setError(job, FileStore::Job::InvalidJobContext, message);
+    }
+
+    int errorCode = 0;
+    QString errorText;
+    checkCollectionFetch(job, errorCode, errorText);
+    if (errorCode != 0) {
+        d->mSession->setError(job, errorCode, errorText);
+    }
+
+    return job;
+}
+
+FileStore::CollectionModifyJob *FileStore::AbstractLocalStore::modifyCollection(const Collection &collection)
+{
+    FileStore::CollectionModifyJob *job = new FileStore::CollectionModifyJob(collection, d->mSession);
+
+    if (d->mTopLevelCollection.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Configured storage location is empty");
+        qCritical() << message;
+        qCritical() << collection;
+        d->mSession->setError(job, FileStore::Job::InvalidStoreState, message);
+    } else if (collection.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Given folder name is empty");
+        qCritical() << message;
+        qCritical() << collection;
+        d->mSession->setError(job, FileStore::Job::InvalidJobContext, message);
+    } else if ((collection.rights() & Collection::CanChangeCollection) == 0) {
+        const QString message = i18nc("@info:status", "Access control prohibits folder modification in folder %1", collection.name());
+        qCritical() << message;
+        qCritical() << collection;
+        d->mSession->setError(job, FileStore::Job::InvalidJobContext, message);
+    }
+
+    int errorCode = 0;
+    QString errorText;
+    checkCollectionModify(job, errorCode, errorText);
+    if (errorCode != 0) {
+        d->mSession->setError(job, errorCode, errorText);
+    }
+
+    return job;
+}
+
+FileStore::CollectionMoveJob *FileStore::AbstractLocalStore::moveCollection(const Collection &collection, const Collection &targetParent)
+{
+    FileStore::CollectionMoveJob *job = new FileStore::CollectionMoveJob(collection, targetParent, d->mSession);
+
+    if (d->mTopLevelCollection.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Configured storage location is empty");
+        qCritical() << message;
+        qCritical() << collection << targetParent;
+        d->mSession->setError(job, FileStore::Job::InvalidStoreState, message);
+    } else if (collection.remoteId().isEmpty() ||
+               collection.parentCollection().remoteId().isEmpty() ||
+               targetParent.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Given folder name is empty");
+        qCritical() << message;
+        qCritical() << collection << targetParent;
+        d->mSession->setError(job, FileStore::Job::InvalidJobContext, message);
+    } else if ((targetParent.rights() & Collection::CanCreateCollection) == 0) {
+        const QString message = i18nc("@info:status", "Access control prohibits folder creation in folder %1", targetParent.name());
+        qCritical() << message;
+        qCritical() << collection << targetParent;
+        d->mSession->setError(job, FileStore::Job::InvalidJobContext, message);
+    }
+
+    int errorCode = 0;
+    QString errorText;
+    checkCollectionMove(job, errorCode, errorText);
+    if (errorCode != 0) {
+        d->mSession->setError(job, errorCode, errorText);
+    }
+
+    return job;
+}
+
+FileStore::ItemFetchJob *FileStore::AbstractLocalStore::fetchItems(const Collection &collection) const
+{
+    FileStore::ItemFetchJob *job = new FileStore::ItemFetchJob(collection, d->mSession);
+
+    if (d->mTopLevelCollection.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Configured storage location is empty");
+        qCritical() << message;
+        qCritical() << collection;
+        d->mSession->setError(job, FileStore::Job::InvalidStoreState, message);
+    } else if (collection.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Given folder name is empty");
+        qCritical() << message;
+        qCritical() << collection;
+        d->mSession->setError(job, FileStore::Job::InvalidJobContext, message);
+    }
+
+    int errorCode = 0;
+    QString errorText;
+    checkItemFetch(job, errorCode, errorText);
+    if (errorCode != 0) {
+        d->mSession->setError(job, errorCode, errorText);
+    }
+
+    return job;
+}
+
+FileStore::ItemFetchJob *FileStore::AbstractLocalStore::fetchItem(const Item &item) const
+{
+    FileStore::ItemFetchJob *job = new FileStore::ItemFetchJob(item, d->mSession);
+
+    if (d->mTopLevelCollection.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Configured storage location is empty");
+        qCritical() << message;
+        qCritical() << "Item(remoteId=" << item.remoteId() << ", mimeType=" << item.mimeType()
+                    << ", parentCollection=" << item.parentCollection().remoteId() << ")";
+        d->mSession->setError(job, FileStore::Job::InvalidStoreState, message);
+    } else if (item.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Given item identifier is empty");
+        qCritical() << message;
+        qCritical() << "Item(remoteId=" << item.remoteId() << ", mimeType=" << item.mimeType()
+                    << ", parentCollection=" << item.parentCollection().remoteId() << ")";
+        d->mSession->setError(job, FileStore::Job::InvalidJobContext, message);
+    }
+
+    int errorCode = 0;
+    QString errorText;
+    checkItemFetch(job, errorCode, errorText);
+    if (errorCode != 0) {
+        d->mSession->setError(job, errorCode, errorText);
+    }
+
+    return job;
+}
+
+FileStore::ItemCreateJob *FileStore::AbstractLocalStore::createItem(const Item &item, const Collection &collection)
+{
+    FileStore::ItemCreateJob *job = new FileStore::ItemCreateJob(item, collection, d->mSession);
+
+    if (d->mTopLevelCollection.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Configured storage location is empty");
+        qCritical() << message;
+        qCritical() << collection
+                    << "Item(remoteId=" << item.remoteId() << ", mimeType=" << item.mimeType()
+                    << ")";
+        d->mSession->setError(job, FileStore::Job::InvalidStoreState, message);
+    } else if (collection.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Given folder name is empty");
+        qCritical() << message;
+        qCritical() << collection
+                    << "Item(remoteId=" << item.remoteId() << ", mimeType=" << item.mimeType()
+                    << ")";
+        d->mSession->setError(job, FileStore::Job::InvalidJobContext, message);
+    } else if ((collection.rights() & Collection::CanCreateItem) == 0) {
+        const QString message = i18nc("@info:status", "Access control prohibits item creation in folder %1", collection.name());
+        qCritical() << message;
+        qCritical() << collection
+                    << "Item(remoteId=" << item.remoteId() << ", mimeType=" << item.mimeType()
+                    << ")";
+        d->mSession->setError(job, FileStore::Job::InvalidJobContext, message);
+    }
+
+    int errorCode = 0;
+    QString errorText;
+    checkItemCreate(job, errorCode, errorText);
+    if (errorCode != 0) {
+        d->mSession->setError(job, errorCode, errorText);
+    }
+
+    return job;
+}
+
+FileStore::ItemModifyJob *FileStore::AbstractLocalStore::modifyItem(const Item &item)
+{
+    FileStore::ItemModifyJob *job = new FileStore::ItemModifyJob(item, d->mSession);
+
+    if (d->mTopLevelCollection.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Configured storage location is empty");
+        qCritical() << message;
+        qCritical() << "Item(remoteId=" << item.remoteId() << ", mimeType=" << item.mimeType()
+                    << ", parentCollection=" << item.parentCollection().remoteId() << ")";
+        d->mSession->setError(job, FileStore::Job::InvalidStoreState, message);
+    } else if (item.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Given item identifier is empty");
+        qCritical() << message;
+        qCritical() << "Item(remoteId=" << item.remoteId() << ", mimeType=" << item.mimeType()
+                    << ", parentCollection=" << item.parentCollection().remoteId() << ")";
+        d->mSession->setError(job, FileStore::Job::InvalidJobContext, message);
+    } else if ((item.parentCollection().rights() & Collection::CanChangeItem) == 0) {
+        const QString message = i18nc("@info:status", "Access control prohibits item modification in folder %1", item.parentCollection().name());
+        qCritical() << message;
+        qCritical() << "Item(remoteId=" << item.remoteId() << ", mimeType=" << item.mimeType()
+                    << ", parentCollection=" << item.parentCollection().remoteId() << ")";
+        d->mSession->setError(job, FileStore::Job::InvalidJobContext, message);
+    }
+
+    int errorCode = 0;
+    QString errorText;
+    checkItemModify(job, errorCode, errorText);
+    if (errorCode != 0) {
+        d->mSession->setError(job, errorCode, errorText);
+    }
+
+    return job;
+}
+
+FileStore::ItemDeleteJob *FileStore::AbstractLocalStore::deleteItem(const Item &item)
+{
+    FileStore::ItemDeleteJob *job = new FileStore::ItemDeleteJob(item, d->mSession);
+
+    if (d->mTopLevelCollection.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Configured storage location is empty");
+        qCritical() << message;
+        qCritical() << "Item(remoteId=" << item.remoteId() << ", mimeType=" << item.mimeType()
+                    << ", parentCollection=" << item.parentCollection().remoteId() << ")";
+        d->mSession->setError(job, FileStore::Job::InvalidStoreState, message);
+    } else if (item.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Given item identifier is empty");
+        qCritical() << message;
+        qCritical() << "Item(remoteId=" << item.remoteId() << ", mimeType=" << item.mimeType()
+                    << ", parentCollection=" << item.parentCollection().remoteId() << ")";
+        d->mSession->setError(job, FileStore::Job::InvalidJobContext, message);
+    } else if ((item.parentCollection().rights() & Collection::CanDeleteItem) == 0) {
+        const QString message = i18nc("@info:status", "Access control prohibits item deletion in folder %1", item.parentCollection().name());
+        qCritical() << message;
+        qCritical() << "Item(remoteId=" << item.remoteId() << ", mimeType=" << item.mimeType()
+                    << ", parentCollection=" << item.parentCollection().remoteId() << ")";
+        d->mSession->setError(job, FileStore::Job::InvalidJobContext, message);
+    }
+
+    int errorCode = 0;
+    QString errorText;
+    checkItemDelete(job, errorCode, errorText);
+    if (errorCode != 0) {
+        d->mSession->setError(job, errorCode, errorText);
+    }
+
+    return job;
+}
+
+FileStore::ItemMoveJob *FileStore::AbstractLocalStore::moveItem(const Item &item, const Collection &targetParent)
+{
+    FileStore::ItemMoveJob *job = new FileStore::ItemMoveJob(item, targetParent, d->mSession);
+
+    if (d->mTopLevelCollection.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Configured storage location is empty");
+        qCritical() << message;
+        qCritical() << "Item(remoteId=" << item.remoteId() << ", mimeType=" << item.mimeType()
+                    << ", parentCollection=" << item.parentCollection().remoteId() << ")"
+                    << targetParent;
+        d->mSession->setError(job, FileStore::Job::InvalidStoreState, message);
+    } else if (item.parentCollection().remoteId().isEmpty() ||
+               targetParent.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Given folder name is empty");
+        qCritical() << message;
+        qCritical() << "Item(remoteId=" << item.remoteId() << ", mimeType=" << item.mimeType()
+                    << ", parentCollection=" << item.parentCollection().remoteId() << ")"
+                    << targetParent;
+        d->mSession->setError(job, FileStore::Job::InvalidJobContext, message);
+    } else if ((targetParent.rights() & Collection::CanCreateItem) == 0) {
+        const QString message = i18nc("@info:status", "Access control prohibits item creation in folder %1", targetParent.name());
+        qCritical() << message;
+        qCritical() << "Item(remoteId=" << item.remoteId() << ", mimeType=" << item.mimeType()
+                    << ", parentCollection=" << item.parentCollection().remoteId() << ")"
+                    << targetParent;
+        d->mSession->setError(job, FileStore::Job::InvalidJobContext, message);
+    } else if ((item.parentCollection().rights() & Collection::CanDeleteItem) == 0) {
+        const QString message = i18nc("@info:status", "Access control prohibits item deletion in folder %1", item.parentCollection().name());
+        qCritical() << message;
+        qCritical() << "Item(remoteId=" << item.remoteId() << ", mimeType=" << item.mimeType()
+                    << ", parentCollection=" << item.parentCollection().remoteId() << ")"
+                    << targetParent;
+        d->mSession->setError(job, FileStore::Job::InvalidJobContext, message);
+    } else if (item.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Given item identifier is empty");
+        qCritical() << message;
+        qCritical() << "Item(remoteId=" << item.remoteId() << ", mimeType=" << item.mimeType()
+                    << ", parentCollection=" << item.parentCollection().remoteId() << ")"
+                    << targetParent;
+        d->mSession->setError(job, FileStore::Job::InvalidJobContext, message);
+    }
+
+    int errorCode = 0;
+    QString errorText;
+    checkItemMove(job, errorCode, errorText);
+    if (errorCode != 0) {
+        d->mSession->setError(job, errorCode, errorText);
+    }
+
+    return job;
+}
+
+FileStore::StoreCompactJob *FileStore::AbstractLocalStore::compactStore()
+{
+    FileStore::StoreCompactJob *job = new FileStore::StoreCompactJob(d->mSession);
+
+    if (d->mTopLevelCollection.remoteId().isEmpty()) {
+        const QString message = i18nc("@info:status", "Configured storage location is empty");
+        qCritical() << message;
+        d->mSession->setError(job, FileStore::Job::InvalidStoreState, message);
+    }
+
+    int errorCode = 0;
+    QString errorText;
+    checkStoreCompact(job, errorCode, errorText);
+    if (errorCode != 0) {
+        d->mSession->setError(job, errorCode, errorText);
+    }
+
+    return job;
+}
+
+FileStore::Job *FileStore::AbstractLocalStore::currentJob() const
+{
+    return d->mCurrentJob;
+}
+
+void FileStore::AbstractLocalStore::notifyError(int errorCode, const QString &errorText) const
+{
+    Q_ASSERT(d->mCurrentJob != 0);
+
+    d->mSession->setError(d->mCurrentJob, errorCode, errorText);
+}
+
+void FileStore::AbstractLocalStore::notifyCollectionsProcessed(const Collection::List &collections) const
+{
+    Q_ASSERT(d->mCurrentJob != 0);
+
+    d->mCollectionsProcessedNotifier.setCollections(collections);
+    d->mCurrentJob->accept(&(d->mCollectionsProcessedNotifier));
+}
+
+void FileStore::AbstractLocalStore::notifyItemsProcessed(const Item::List &items) const
+{
+    Q_ASSERT(d->mCurrentJob != 0);
+
+    d->mItemsProcessedNotifier.setItems(items);
+    d->mCurrentJob->accept(&(d->mItemsProcessedNotifier));
+    d->mItemsProcessedNotifier.clearItems(); // save memory
+}
+
+void FileStore::AbstractLocalStore::setTopLevelCollection(const Collection &collection)
+{
+    d->mTopLevelCollection = collection;
+    d->mTopLevelCollectionFetcher.setTopLevelCollection(collection);
+}
+
+void FileStore::AbstractLocalStore::checkCollectionCreate(FileStore::CollectionCreateJob *job, int &errorCode, QString &errorText) const
+{
+    Q_UNUSED(job);
+    Q_UNUSED(errorCode);
+    Q_UNUSED(errorText);
+}
+
+void FileStore::AbstractLocalStore::checkCollectionDelete(FileStore::CollectionDeleteJob *job, int &errorCode, QString &errorText) const
+{
+    Q_UNUSED(job);
+    Q_UNUSED(errorCode);
+    Q_UNUSED(errorText);
+}
+
+void FileStore::AbstractLocalStore::checkCollectionFetch(FileStore::CollectionFetchJob *job, int &errorCode, QString &errorText) const
+{
+    Q_UNUSED(job);
+    Q_UNUSED(errorCode);
+    Q_UNUSED(errorText);
+}
+
+void FileStore::AbstractLocalStore::checkCollectionModify(FileStore::CollectionModifyJob *job, int &errorCode, QString &errorText) const
+{
+    Q_UNUSED(job);
+    Q_UNUSED(errorCode);
+    Q_UNUSED(errorText);
+}
+
+void FileStore::AbstractLocalStore::checkCollectionMove(FileStore::CollectionMoveJob *job, int &errorCode, QString &errorText) const
+{
+    Q_UNUSED(job);
+    Q_UNUSED(errorCode);
+    Q_UNUSED(errorText);
+}
+
+void FileStore::AbstractLocalStore::checkItemCreate(FileStore::ItemCreateJob *job, int &errorCode, QString &errorText) const
+{
+    Q_UNUSED(job);
+    Q_UNUSED(errorCode);
+    Q_UNUSED(errorText);
+}
+
+void FileStore::AbstractLocalStore::checkItemDelete(FileStore::ItemDeleteJob *job, int &errorCode, QString &errorText) const
+{
+    Q_UNUSED(job);
+    Q_UNUSED(errorCode);
+    Q_UNUSED(errorText);
+}
+
+void FileStore::AbstractLocalStore::checkItemFetch(FileStore::ItemFetchJob *job, int &errorCode, QString &errorText) const
+{
+    Q_UNUSED(job);
+    Q_UNUSED(errorCode);
+    Q_UNUSED(errorText);
+}
+
+void FileStore::AbstractLocalStore::checkItemModify(FileStore::ItemModifyJob *job, int &errorCode, QString &errorText) const
+{
+    Q_UNUSED(job);
+    Q_UNUSED(errorCode);
+    Q_UNUSED(errorText);
+}
+
+void FileStore::AbstractLocalStore::checkItemMove(FileStore::ItemMoveJob *job, int &errorCode, QString &errorText) const
+{
+    Q_UNUSED(job);
+    Q_UNUSED(errorCode);
+    Q_UNUSED(errorText);
+}
+
+void FileStore::AbstractLocalStore::checkStoreCompact(FileStore::StoreCompactJob *job, int &errorCode, QString &errorText) const
+{
+    Q_UNUSED(job);
+    Q_UNUSED(errorCode);
+    Q_UNUSED(errorText);
+}
+
+#include "moc_abstractlocalstore.cpp"
+
diff --git a/resources/shared/filestore/abstractlocalstore.h b/resources/shared/filestore/abstractlocalstore.h
new file mode 100644 (file)
index 0000000..63d13b3
--- /dev/null
@@ -0,0 +1,125 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef AKONADI_FILESTORE_ABSTRACTLOCALSTORE_H
+#define AKONADI_FILESTORE_ABSTRACTLOCALSTORE_H
+
+#include "storeinterface.h"
+
+#include <Collection>
+#include <Item>
+
+#include <QObject>
+
+template <typename T> class QList;
+
+namespace Akonadi
+{
+
+namespace FileStore
+{
+
+/**
+ */
+class AKONADI_FILESTORE_EXPORT AbstractLocalStore : public QObject, public StoreInterface
+{
+    Q_OBJECT
+
+public:
+    AbstractLocalStore();
+    ~AbstractLocalStore();
+
+    virtual void setPath(const QString &path);
+    QString path() const;
+
+    Collection topLevelCollection() const Q_DECL_OVERRIDE;
+
+    CollectionCreateJob *createCollection(const Collection &collection, const Collection &targetParent) Q_DECL_OVERRIDE;
+
+    CollectionFetchJob *fetchCollections(const Collection &collection, CollectionFetchJob::Type type = CollectionFetchJob::FirstLevel) const Q_DECL_OVERRIDE;
+
+    CollectionDeleteJob *deleteCollection(const Collection &collection) Q_DECL_OVERRIDE;
+
+    CollectionModifyJob *modifyCollection(const Collection &collection) Q_DECL_OVERRIDE;
+
+    CollectionMoveJob *moveCollection(const Collection &collection, const Collection &targetParent) Q_DECL_OVERRIDE;
+
+    ItemFetchJob *fetchItems(const Collection &collection) const Q_DECL_OVERRIDE;
+
+    ItemFetchJob *fetchItem(const Item &item) const Q_DECL_OVERRIDE;
+
+    ItemCreateJob *createItem(const Item &item, const Collection &collection) Q_DECL_OVERRIDE;
+
+    ItemModifyJob *modifyItem(const Item &item) Q_DECL_OVERRIDE;
+
+    ItemDeleteJob *deleteItem(const Item &item) Q_DECL_OVERRIDE;
+
+    ItemMoveJob *moveItem(const Item &item, const Collection &targetParent) Q_DECL_OVERRIDE;
+
+    StoreCompactJob *compactStore() Q_DECL_OVERRIDE;
+
+protected: // job processing
+    virtual void processJob(Job *job) = 0;
+
+    Job *currentJob() const;
+
+    void notifyError(int errorCode, const QString &errorText) const;
+
+    void notifyCollectionsProcessed(const Collection::List &collections) const;
+
+    void notifyItemsProcessed(const Item::List &items) const;
+
+protected: // template methods
+    void setTopLevelCollection(const Collection &collection) Q_DECL_OVERRIDE;
+
+    virtual void checkCollectionCreate(CollectionCreateJob *job, int &errorCode, QString &errorText) const;
+
+    virtual void checkCollectionDelete(CollectionDeleteJob *job, int &errorCode, QString &errorText) const;
+
+    virtual void checkCollectionFetch(CollectionFetchJob *job, int &errorCode, QString &errorText) const;
+
+    virtual void checkCollectionModify(CollectionModifyJob *job, int &errorCode, QString &errorText) const;
+
+    virtual void checkCollectionMove(CollectionMoveJob *job, int &errorCode, QString &errorText) const;
+
+    virtual void checkItemCreate(ItemCreateJob *job, int &errorCode, QString &errorText) const;
+
+    virtual void checkItemDelete(ItemDeleteJob *job, int &errorCode, QString &errorText) const;
+
+    virtual void checkItemFetch(ItemFetchJob *job, int &errorCode, QString &errorText) const;
+
+    virtual void checkItemModify(ItemModifyJob *job, int &errorCode, QString &errorText) const;
+
+    virtual void checkItemMove(ItemMoveJob *job, int &errorCode, QString &errorText) const;
+
+    virtual void checkStoreCompact(StoreCompactJob *job, int &errorCode, QString &errorText) const;
+
+private:
+    class Private;
+    Private *const d;
+
+    Q_PRIVATE_SLOT(d, void processJobs(const QList<FileStore::Job *> &jobs))
+};
+
+}
+}
+
+#endif
+
diff --git a/resources/shared/filestore/autotests/CMakeLists.txt b/resources/shared/filestore/autotests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..590a171
--- /dev/null
@@ -0,0 +1,29 @@
+include(ECMAddTests)
+
+find_package(Qt5Test CONFIG REQUIRED)
+
+if(${EXECUTABLE_OUTPUT_PATH})
+    set( PREVIOUS_EXEC_OUTPUT_PATH ${EXECUTABLE_OUTPUT_PATH} )
+else()
+    set( PREVIOUS_EXEC_OUTPUT_PATH . )
+endif()
+set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} )
+
+
+
+include_directories(
+  ${CMAKE_CURRENT_SOURCE_DIR}/../
+  ${CMAKE_CURRENT_BINARY_DIR}/../
+  ${CMAKE_CURRENT_SOURCE_DIR}
+)
+
+add_executable(abstractlocalstoretest abstractlocalstoretest.cpp)
+add_test(abstractlocalstoretest abstractlocalstoretest)
+ecm_mark_as_test(abstractlocalstoretest)
+
+target_link_libraries(
+  abstractlocalstoretest
+  akonadi-filestore
+  KF5::AkonadiCore
+  Qt5::Test
+)
diff --git a/resources/shared/filestore/autotests/abstractlocalstoretest.cpp b/resources/shared/filestore/autotests/abstractlocalstoretest.cpp
new file mode 100644 (file)
index 0000000..dd048bf
--- /dev/null
@@ -0,0 +1,971 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "abstractlocalstore.h"
+
+#include "collectioncreatejob.h"
+#include "collectiondeletejob.h"
+#include "collectionfetchjob.h"
+#include "collectionmodifyjob.h"
+#include "collectionmovejob.h"
+#include "itemcreatejob.h"
+#include "itemdeletejob.h"
+#include "itemfetchjob.h"
+#include "itemmodifyjob.h"
+#include "itemmovejob.h"
+#include "sessionimpls_p.h"
+#include "storecompactjob.h"
+
+#include <KRandom>
+
+#include <qtest.h>
+
+using namespace Akonadi;
+using namespace Akonadi::FileStore;
+
+class TestStore : public AbstractLocalStore
+{
+    Q_OBJECT
+
+public:
+    TestStore() : mLastCheckedJob(0), mLastProcessedJob(0), mErrorCode(0) {}
+
+public:
+    mutable Akonadi::FileStore::Job *mLastCheckedJob;
+    Akonadi::FileStore::Job *mLastProcessedJob;
+
+    Collection mTopLevelCollection;
+
+    int mErrorCode;
+    QString mErrorText;
+
+protected:
+    void processJob(Akonadi::FileStore::Job *job);
+
+protected:
+    void setTopLevelCollection(const Collection &collection)
+    {
+        mTopLevelCollection = collection;
+
+        Collection modifiedCollection = collection;
+        modifiedCollection.setContentMimeTypes(QStringList() << Collection::mimeType());
+
+        AbstractLocalStore::setTopLevelCollection(modifiedCollection);
+    }
+
+    void checkCollectionCreate(Akonadi::FileStore::CollectionCreateJob *job, int &errorCode, QString &errorText) const
+    {
+        mLastCheckedJob = job;
+        errorCode = mErrorCode;
+        errorText = mErrorText;
+    }
+
+    void checkCollectionDelete(Akonadi::FileStore::CollectionDeleteJob *job, int &errorCode, QString &errorText) const
+    {
+        mLastCheckedJob = job;
+        errorCode = mErrorCode;
+        errorText = mErrorText;
+    }
+
+    void checkCollectionFetch(Akonadi::FileStore::CollectionFetchJob *job, int &errorCode, QString &errorText) const
+    {
+        mLastCheckedJob = job;
+        errorCode = mErrorCode;
+        errorText = mErrorText;
+    }
+
+    void checkCollectionModify(Akonadi::FileStore::CollectionModifyJob *job, int &errorCode, QString &errorText) const
+    {
+        mLastCheckedJob = job;
+        errorCode = mErrorCode;
+        errorText = mErrorText;
+    }
+
+    void checkCollectionMove(Akonadi::FileStore::CollectionMoveJob *job, int &errorCode, QString &errorText) const
+    {
+        mLastCheckedJob = job;
+        errorCode = mErrorCode;
+        errorText = mErrorText;
+    }
+
+    void checkItemCreate(Akonadi::FileStore::ItemCreateJob *job, int &errorCode, QString &errorText) const
+    {
+        mLastCheckedJob = job;
+        errorCode = mErrorCode;
+        errorText = mErrorText;
+    }
+
+    void checkItemDelete(Akonadi::FileStore::ItemDeleteJob *job, int &errorCode, QString &errorText) const
+    {
+        mLastCheckedJob = job;
+        errorCode = mErrorCode;
+        errorText = mErrorText;
+    }
+
+    void checkItemFetch(Akonadi::FileStore::ItemFetchJob *job, int &errorCode, QString &errorText) const
+    {
+        mLastCheckedJob = job;
+        errorCode = mErrorCode;
+        errorText = mErrorText;
+    }
+
+    void checkItemModify(Akonadi::FileStore::ItemModifyJob *job, int &errorCode, QString &errorText) const
+    {
+        mLastCheckedJob = job;
+        errorCode = mErrorCode;
+        errorText = mErrorText;
+    }
+
+    void checkItemMove(Akonadi::FileStore::ItemMoveJob *job, int &errorCode, QString &errorText) const
+    {
+        mLastCheckedJob = job;
+        errorCode = mErrorCode;
+        errorText = mErrorText;
+    }
+
+    void checkStoreCompact(Akonadi::FileStore::StoreCompactJob *job, int &errorCode, QString &errorText) const
+    {
+        mLastCheckedJob = job;
+        errorCode = mErrorCode;
+        errorText = mErrorText;
+    }
+};
+
+void TestStore::processJob(Akonadi::FileStore::Job *job)
+{
+    mLastProcessedJob = job;
+
+    QCOMPARE(currentJob(), job);
+    QVERIFY(job->error() == 0);
+
+    if (mErrorCode != 0) {
+        notifyError(mErrorCode, mErrorText);
+    }
+}
+
+class AbstractLocalStoreTest : public QObject
+{
+    Q_OBJECT
+
+public:
+    AbstractLocalStoreTest() : QObject(), mStore(0) {}
+    ~AbstractLocalStoreTest()
+    {
+        delete mStore;
+    }
+
+private:
+    TestStore *mStore;
+
+private Q_SLOTS:
+    void init();
+    void testSetPath();
+    void testCreateCollection();
+    void testDeleteCollection();
+    void testFetchCollection();
+    void testModifyCollection();
+    void testMoveCollection();
+    void testFetchItems();
+    void testFetchItem();
+    void testCreateItem();
+    void testDeleteItem();
+    void testModifyItem();
+    void testMoveItem();
+    void testCompactStore();
+};
+
+void AbstractLocalStoreTest::init()
+{
+    delete mStore;
+    mStore = new TestStore;
+}
+
+void AbstractLocalStoreTest::testSetPath()
+{
+    const QString file = KRandom::randomString(10);
+    const QString path = QLatin1String("/tmp/test/") + file;
+
+    // check that setTopLevelCollection() has been called
+    mStore->setPath(path);
+    QCOMPARE(mStore->mTopLevelCollection.remoteId(), path);
+
+    // check that the modified collection is the one returned by topLevelCollection()
+    QVERIFY(mStore->mTopLevelCollection.contentMimeTypes().isEmpty());
+    QCOMPARE(mStore->topLevelCollection().remoteId(), path);
+    QCOMPARE(mStore->topLevelCollection().contentMimeTypes(), QStringList() << Collection::mimeType());
+    QCOMPARE(mStore->topLevelCollection().name(), file);
+
+    // check that calling with the same path again, does not call the template method
+    mStore->mTopLevelCollection = Collection();
+    mStore->setPath(path);
+    QVERIFY(mStore->mTopLevelCollection.remoteId().isEmpty());
+    QCOMPARE(mStore->topLevelCollection().remoteId(), path);
+    QCOMPARE(mStore->topLevelCollection().contentMimeTypes(), QStringList() << Collection::mimeType());
+    QCOMPARE(mStore->topLevelCollection().name(), file);
+
+    // check that calling with a different path works like the first call
+    const QString file2 = KRandom::randomString(10);
+    const QString path2 = QLatin1String("/tmp/test2/") + file2;
+
+    mStore->setPath(path2);
+    QCOMPARE(mStore->mTopLevelCollection.remoteId(), path2);
+    QCOMPARE(mStore->topLevelCollection().remoteId(), path2);
+    QCOMPARE(mStore->topLevelCollection().contentMimeTypes(), QStringList() << Collection::mimeType());
+    QCOMPARE(mStore->topLevelCollection().name(), file2);
+}
+
+void AbstractLocalStoreTest::testCreateCollection()
+{
+    CollectionCreateJob *job = 0;
+
+    // test without setPath()
+    job = mStore->createCollection(Collection(), Collection());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidStoreState);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with path but with invalid collections
+    mStore->setPath(QStringLiteral("/tmp/test"));
+    job = mStore->createCollection(Collection(), Collection());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidJobContext);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with potentially valid collection (has remoteId), but invalid target parent
+    Collection collection;
+    collection.setRemoteId(QStringLiteral("/tmp/test/foo"));
+    job = mStore->createCollection(collection, Collection());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidJobContext);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with potentially valid collections
+    Collection targetParent;
+    targetParent.setRemoteId(QStringLiteral("/tmp/test2"));
+    job = mStore->createCollection(collection, targetParent);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), 0);
+    QVERIFY(job->errorText().isEmpty());
+
+    QVERIFY(job->exec());
+    QCOMPARE(mStore->mLastProcessedJob, job);
+    mStore->mLastProcessedJob = 0;
+
+    // test template check method
+    mStore->mErrorCode = KRandom::random() + 1;
+    mStore->mErrorText = KRandom::randomString(10);
+
+    job = mStore->createCollection(collection, targetParent);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), mStore->mErrorCode);
+    QCOMPARE(job->errorText(), mStore->mErrorText);
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+    mStore->mErrorCode = 0;
+    mStore->mErrorText = QString();
+}
+
+void AbstractLocalStoreTest::testDeleteCollection()
+{
+    CollectionDeleteJob *job = 0;
+
+    // test without setPath()
+    job = mStore->deleteCollection(Collection());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidStoreState);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with path but with invalid collection
+    mStore->setPath(QStringLiteral("/tmp/test"));
+    job = mStore->deleteCollection(Collection());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidJobContext);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with ivalid collection (has remoteId, but no parent collection remoteId)
+    Collection collection;
+    collection.setRemoteId(QStringLiteral("/tmp/test/foo"));
+    job = mStore->deleteCollection(collection);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidJobContext);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with potentially valid collection (has remoteId and parent collection remoteId)
+    Collection parentCollection;
+    parentCollection.setRemoteId(QStringLiteral("/tmp/test"));
+    collection.setParentCollection(parentCollection);
+    job = mStore->deleteCollection(collection);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), 0);
+    QVERIFY(job->errorText().isEmpty());
+
+    QVERIFY(job->exec());
+    QCOMPARE(mStore->mLastProcessedJob, job);
+    mStore->mLastProcessedJob = 0;
+
+    // test template check method
+    mStore->mErrorCode = KRandom::random() + 1;
+    mStore->mErrorText = KRandom::randomString(10);
+
+    job = mStore->deleteCollection(collection);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), mStore->mErrorCode);
+    QCOMPARE(job->errorText(), mStore->mErrorText);
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+    mStore->mErrorCode = 0;
+    mStore->mErrorText = QString();
+}
+
+void AbstractLocalStoreTest::testFetchCollection()
+{
+    Akonadi::FileStore::CollectionFetchJob *job = 0;
+
+    // test without setPath()
+    job = mStore->fetchCollections(Collection());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidStoreState);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with path but with invalid collection
+    mStore->setPath(QStringLiteral("/tmp/test"));
+    job = mStore->fetchCollections(Collection());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidJobContext);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with potentially valid collection (has remoteId)
+    Collection collection;
+    collection.setRemoteId(QStringLiteral("/tmp/test/foo"));
+    job = mStore->fetchCollections(collection);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), 0);
+    QVERIFY(job->errorText().isEmpty());
+
+    QVERIFY(job->exec());
+    QCOMPARE(mStore->mLastProcessedJob, job);
+    mStore->mLastProcessedJob = 0;
+
+    // test template check method
+    mStore->mErrorCode = KRandom::random() + 1;
+    mStore->mErrorText = KRandom::randomString(10);
+
+    job = mStore->fetchCollections(collection);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), mStore->mErrorCode);
+    QCOMPARE(job->errorText(), mStore->mErrorText);
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+    mStore->mErrorCode = 0;
+    mStore->mErrorText = QString();
+
+    // test fetch of top level collection only
+    collection.setRemoteId(mStore->topLevelCollection().remoteId());
+    job = mStore->fetchCollections(collection, Akonadi::FileStore::CollectionFetchJob::Base);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), 0);
+    QVERIFY(job->errorText().isEmpty());
+
+    QVERIFY(job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);   // job not handed to subclass because it is full processed in base class
+    QCOMPARE(job->collections().count(), 1);
+    QCOMPARE(job->collections()[ 0 ], mStore->topLevelCollection());
+}
+
+void AbstractLocalStoreTest::testModifyCollection()
+{
+    Akonadi::FileStore::CollectionModifyJob *job = 0;
+
+    // test without setPath()
+    job = mStore->modifyCollection(Collection());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidStoreState);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with path but with invalid item
+    mStore->setPath(QStringLiteral("/tmp/test"));
+    job = mStore->modifyCollection(Collection());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidJobContext);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with potentially valid collection (has remoteId, but no parent remoteId)
+    Collection collection;
+    collection.setRemoteId(QStringLiteral("/tmp/test/foo"));
+    job = mStore->modifyCollection(collection);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), 0);
+    QVERIFY(job->errorText().isEmpty());
+
+    QVERIFY(job->exec());
+    QCOMPARE(mStore->mLastProcessedJob, job);
+    mStore->mLastProcessedJob = 0;
+
+    // test with potentially valid collection (has remoteId and parent remoteId)
+    Collection parentCollection;
+    parentCollection.setRemoteId(QStringLiteral("/tmp/test"));
+    collection.setParentCollection(parentCollection);
+    job = mStore->modifyCollection(collection);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), 0);
+    QVERIFY(job->errorText().isEmpty());
+
+    QVERIFY(job->exec());
+    QCOMPARE(mStore->mLastProcessedJob, job);
+    mStore->mLastProcessedJob = 0;
+
+    // test template check method
+    mStore->mErrorCode = KRandom::random() + 1;
+    mStore->mErrorText = KRandom::randomString(10);
+
+    job = mStore->modifyCollection(collection);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), mStore->mErrorCode);
+    QCOMPARE(job->errorText(), mStore->mErrorText);
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+}
+
+void AbstractLocalStoreTest::testMoveCollection()
+{
+    CollectionMoveJob *job = 0;
+
+    // test without setPath()
+    job = mStore->moveCollection(Collection(), Collection());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidStoreState);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with path but with invalid collections
+    mStore->setPath(QStringLiteral("/tmp/test"));
+    job = mStore->moveCollection(Collection(), Collection());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidJobContext);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with potentially valid collection (has remoteId and parent remoteId), but invalid target parent
+    Collection collection;
+    collection.setRemoteId(QStringLiteral("/tmp/test/foo"));
+    Collection parentCollection;
+    parentCollection.setRemoteId(QStringLiteral("/tmp/test"));
+    collection.setParentCollection(parentCollection);
+    job = mStore->moveCollection(collection, Collection());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidJobContext);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with invalid parent collection, but with potentially valid collection and target parent
+    collection.setParentCollection(Collection());
+    job = mStore->moveCollection(collection, parentCollection);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidJobContext);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with potentially valid collections
+    Collection targetParent;
+    targetParent.setRemoteId(QStringLiteral("/tmp/test2"));
+    collection.setParentCollection(parentCollection);
+    job = mStore->moveCollection(collection, targetParent);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), 0);
+    QVERIFY(job->errorText().isEmpty());
+
+    QVERIFY(job->exec());
+    QCOMPARE(mStore->mLastProcessedJob, job);
+    mStore->mLastProcessedJob = 0;
+
+    // test template check method
+    mStore->mErrorCode = KRandom::random() + 1;
+    mStore->mErrorText = KRandom::randomString(10);
+
+    job = mStore->moveCollection(collection, targetParent);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), mStore->mErrorCode);
+    QCOMPARE(job->errorText(), mStore->mErrorText);
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+}
+
+void AbstractLocalStoreTest::testFetchItems()
+{
+    ItemFetchJob *job = 0;
+
+    // test without setPath()
+    job = mStore->fetchItems(Collection());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidStoreState);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with path but with invalid collection
+    mStore->setPath(QStringLiteral("/tmp/test"));
+    job = mStore->fetchItems(Collection());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidJobContext);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with potentially valid collection (has remoteId)
+    Collection collection;
+    collection.setRemoteId(QStringLiteral("/tmp/test/foo"));
+    job = mStore->fetchItems(collection);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), 0);
+    QVERIFY(job->errorText().isEmpty());
+
+    QVERIFY(job->exec());
+    QCOMPARE(mStore->mLastProcessedJob, job);
+    mStore->mLastProcessedJob = 0;
+
+    // test template check method
+    mStore->mErrorCode = KRandom::random() + 1;
+    mStore->mErrorText = KRandom::randomString(10);
+
+    job = mStore->fetchItems(collection);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), mStore->mErrorCode);
+    QCOMPARE(job->errorText(), mStore->mErrorText);
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+    mStore->mErrorCode = 0;
+    mStore->mErrorText = QString();
+}
+
+void AbstractLocalStoreTest::testFetchItem()
+{
+    ItemFetchJob *job = 0;
+
+    // test without setPath()
+    job = mStore->fetchItem(Item());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidStoreState);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with path but with invalid item
+    mStore->setPath(QStringLiteral("/tmp/test"));
+    job = mStore->fetchItem(Item());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidJobContext);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with potentially valid item (has remoteId)
+    Item item;
+    item.setRemoteId(QStringLiteral("/tmp/test/foo"));
+    job = mStore->fetchItem(item);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), 0);
+    QVERIFY(job->errorText().isEmpty());
+
+    QVERIFY(job->exec());
+    QCOMPARE(mStore->mLastProcessedJob, job);
+    mStore->mLastProcessedJob = 0;
+
+    // test template check method
+    mStore->mErrorCode = KRandom::random() + 1;
+    mStore->mErrorText = KRandom::randomString(10);
+
+    job = mStore->fetchItem(item);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), mStore->mErrorCode);
+    QCOMPARE(job->errorText(), mStore->mErrorText);
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+    mStore->mErrorCode = 0;
+    mStore->mErrorText = QString();
+}
+
+void AbstractLocalStoreTest::testCreateItem()
+{
+    Akonadi::FileStore::ItemCreateJob *job = 0;
+
+    // test without setPath()
+    job = mStore->createItem(Item(), Collection());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidStoreState);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with path but with invalid collection
+    mStore->setPath(QStringLiteral("/tmp/test"));
+    job = mStore->createItem(Item(), Collection());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidJobContext);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with potentially valid collection (has remoteId)
+    Collection collection;
+    collection.setRemoteId(QStringLiteral("/tmp/test/foo"));
+    job = mStore->createItem(Item(), collection);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), 0);
+    QVERIFY(job->errorText().isEmpty());
+
+    QVERIFY(job->exec());
+    QCOMPARE(mStore->mLastProcessedJob, job);
+    mStore->mLastProcessedJob = 0;
+
+    // test template check method
+    mStore->mErrorCode = KRandom::random() + 1;
+    mStore->mErrorText = KRandom::randomString(10);
+
+    job = mStore->createItem(Item(), collection);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), mStore->mErrorCode);
+    QCOMPARE(job->errorText(), mStore->mErrorText);
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+    mStore->mErrorCode = 0;
+    mStore->mErrorText = QString();
+}
+
+void AbstractLocalStoreTest::testDeleteItem()
+{
+    ItemDeleteJob *job = 0;
+
+    // test without setPath()
+    job = mStore->deleteItem(Item());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidStoreState);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with path but with invalid item
+    mStore->setPath(QStringLiteral("/tmp/test"));
+    job = mStore->deleteItem(Item());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidJobContext);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with potentially valid item (has remoteId)
+    Item item;
+    item.setRemoteId(QStringLiteral("/tmp/test/foo"));
+    job = mStore->deleteItem(item);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), 0);
+    QVERIFY(job->errorText().isEmpty());
+
+    QVERIFY(job->exec());
+    QCOMPARE(mStore->mLastProcessedJob, job);
+    mStore->mLastProcessedJob = 0;
+
+    // test template check method
+    mStore->mErrorCode = KRandom::random() + 1;
+    mStore->mErrorText = KRandom::randomString(10);
+
+    job = mStore->deleteItem(item);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), mStore->mErrorCode);
+    QCOMPARE(job->errorText(), mStore->mErrorText);
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+    mStore->mErrorCode = 0;
+    mStore->mErrorText = QString();
+}
+
+void AbstractLocalStoreTest::testModifyItem()
+{
+    Akonadi::FileStore::ItemModifyJob *job = 0;
+
+    // test without setPath()
+    job = mStore->modifyItem(Item());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidStoreState);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with path but with invalid item
+    mStore->setPath(QStringLiteral("/tmp/test"));
+    job = mStore->modifyItem(Item());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidJobContext);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with potentially valid item (has remoteId)
+    Item item;
+    item.setRemoteId(QStringLiteral("/tmp/test/foo"));
+    job = mStore->modifyItem(item);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), 0);
+    QVERIFY(job->errorText().isEmpty());
+
+    QVERIFY(job->exec());
+    QCOMPARE(mStore->mLastProcessedJob, job);
+    mStore->mLastProcessedJob = 0;
+
+    // test template check method
+    mStore->mErrorCode = KRandom::random() + 1;
+    mStore->mErrorText = KRandom::randomString(10);
+
+    job = mStore->modifyItem(item);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), mStore->mErrorCode);
+    QCOMPARE(job->errorText(), mStore->mErrorText);
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+}
+
+void AbstractLocalStoreTest::testMoveItem()
+{
+    ItemMoveJob *job = 0;
+
+    // test without setPath()
+    job = mStore->moveItem(Item(), Collection());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidStoreState);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with path but with invalid item and collection
+    mStore->setPath(QStringLiteral("/tmp/test"));
+    job = mStore->moveItem(Item(), Collection());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidJobContext);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with potentially valid item (has remoteId and parent remoteId), but invalid target parent
+    Item item;
+    item.setRemoteId(QStringLiteral("/tmp/test/foo"));
+    Collection parentCollection;
+    parentCollection.setRemoteId(QStringLiteral("/tmp/test"));
+    item.setParentCollection(parentCollection);
+    job = mStore->moveItem(item, Collection());
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidJobContext);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with invalid parent collection, but with potentially valid item and target parent
+    item.setParentCollection(Collection());
+    job = mStore->moveItem(item, parentCollection);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidJobContext);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with potentially valid item and collections
+    Collection targetParent;
+    targetParent.setRemoteId(QStringLiteral("/tmp/test2"));
+    item.setParentCollection(parentCollection);
+    job = mStore->moveItem(item, targetParent);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), 0);
+    QVERIFY(job->errorText().isEmpty());
+
+    QVERIFY(job->exec());
+    QCOMPARE(mStore->mLastProcessedJob, job);
+    mStore->mLastProcessedJob = 0;
+
+    // test template check method
+    mStore->mErrorCode = KRandom::random() + 1;
+    mStore->mErrorText = KRandom::randomString(10);
+
+    job = mStore->moveItem(item, targetParent);
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), mStore->mErrorCode);
+    QCOMPARE(job->errorText(), mStore->mErrorText);
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+}
+
+void AbstractLocalStoreTest::testCompactStore()
+{
+    StoreCompactJob *job = 0;
+
+    // test without setPath()
+    job = mStore->compactStore();
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), (int)Akonadi::FileStore::Job::InvalidStoreState);
+    QVERIFY(!job->errorText().isEmpty());
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+
+    // test with path
+    mStore->setPath(QStringLiteral("/tmp/test"));
+    job = mStore->compactStore();
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), 0);
+    QVERIFY(job->errorText().isEmpty());
+
+    QVERIFY(job->exec());
+    QCOMPARE(mStore->mLastProcessedJob, job);
+    mStore->mLastProcessedJob = 0;
+
+    // test template check method
+    mStore->mErrorCode = KRandom::random() + 1;
+    mStore->mErrorText = KRandom::randomString(10);
+
+    job = mStore->compactStore();
+    QVERIFY(job != 0);
+    QCOMPARE(mStore->mLastCheckedJob, job);
+    QCOMPARE(job->error(), mStore->mErrorCode);
+    QCOMPARE(job->errorText(), mStore->mErrorText);
+
+    QVERIFY(!job->exec());
+    QVERIFY(mStore->mLastProcessedJob == 0);
+    mStore->mErrorCode = 0;
+    mStore->mErrorText = QString();
+}
+
+QTEST_MAIN(AbstractLocalStoreTest)
+
+#include "abstractlocalstoretest.moc"
+
diff --git a/resources/shared/filestore/collectioncreatejob.cpp b/resources/shared/filestore/collectioncreatejob.cpp
new file mode 100644 (file)
index 0000000..44fcd3c
--- /dev/null
@@ -0,0 +1,68 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "collectioncreatejob.h"
+
+#include "session_p.h"
+
+using namespace Akonadi;
+
+class FileStore::CollectionCreateJob::Private
+{
+public:
+    Collection mCollection;
+    Collection mTargetParent;
+};
+
+FileStore::CollectionCreateJob::CollectionCreateJob(const Collection &collection, const Collection &targetParent, FileStore::AbstractJobSession *session)
+    : FileStore::Job(session), d(new Private())
+{
+    Q_ASSERT(session != 0);
+
+    d->mCollection = collection;
+    d->mTargetParent = targetParent;
+
+    session->addJob(this);
+}
+
+FileStore::CollectionCreateJob::~CollectionCreateJob()
+{
+    delete d;
+}
+
+Collection FileStore::CollectionCreateJob::collection() const
+{
+    return d->mCollection;
+}
+
+Collection FileStore::CollectionCreateJob::targetParent() const
+{
+    return d->mTargetParent;
+}
+
+bool FileStore::CollectionCreateJob::accept(FileStore::Job::Visitor *visitor)
+{
+    return visitor->visit(this);
+}
+
+void FileStore::CollectionCreateJob::handleCollectionCreated(const Collection &collection)
+{
+    d->mCollection = collection;
+}
diff --git a/resources/shared/filestore/collectioncreatejob.h b/resources/shared/filestore/collectioncreatejob.h
new file mode 100644 (file)
index 0000000..0dc945f
--- /dev/null
@@ -0,0 +1,65 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef AKONADI_FILESTORE_COLLECTIONCREATEJOB_H
+#define AKONADI_FILESTORE_COLLECTIONCREATEJOB_H
+
+#include "job.h"
+
+namespace Akonadi
+{
+class Collection;
+
+namespace FileStore
+{
+class AbstractJobSession;
+
+/**
+ */
+class AKONADI_FILESTORE_EXPORT CollectionCreateJob : public Job
+{
+    friend class AbstractJobSession;
+
+    Q_OBJECT
+
+public:
+    explicit CollectionCreateJob(const Collection &collection, const Collection &targetParent, AbstractJobSession *session = Q_NULLPTR);
+
+    virtual ~CollectionCreateJob();
+
+    Collection collection() const;
+
+    Collection targetParent() const;
+
+    bool accept(Visitor *visitor) Q_DECL_OVERRIDE;
+
+private:
+    void handleCollectionCreated(const Collection &collection);
+
+private:
+    class Private;
+    Private *const d;
+};
+
+}
+}
+
+#endif
+
diff --git a/resources/shared/filestore/collectiondeletejob.cpp b/resources/shared/filestore/collectiondeletejob.cpp
new file mode 100644 (file)
index 0000000..9f2f7d5
--- /dev/null
@@ -0,0 +1,62 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "collectiondeletejob.h"
+
+#include "session_p.h"
+
+using namespace Akonadi;
+
+class FileStore::CollectionDeleteJob::Private
+{
+public:
+    Collection mCollection;
+};
+
+FileStore::CollectionDeleteJob::CollectionDeleteJob(const Collection &collection, FileStore::AbstractJobSession *session)
+    : FileStore::Job(session), d(new Private())
+{
+    Q_ASSERT(session != 0);
+
+    d->mCollection = collection;
+
+    session->addJob(this);
+}
+
+FileStore::CollectionDeleteJob::~CollectionDeleteJob()
+{
+    delete d;
+}
+
+Collection FileStore::CollectionDeleteJob::collection() const
+{
+    return d->mCollection;
+}
+
+bool FileStore::CollectionDeleteJob::accept(FileStore::Job::Visitor *visitor)
+{
+    return visitor->visit(this);
+}
+
+void FileStore::CollectionDeleteJob::handleCollectionDeleted(const Collection &collection)
+{
+    d->mCollection = collection;
+}
+
diff --git a/resources/shared/filestore/collectiondeletejob.h b/resources/shared/filestore/collectiondeletejob.h
new file mode 100644 (file)
index 0000000..4a324b3
--- /dev/null
@@ -0,0 +1,63 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef AKONADI_FILESTORE_COLLECTIONDELETEJOB_H
+#define AKONADI_FILESTORE_COLLECTIONDELETEJOB_H
+
+#include "job.h"
+
+namespace Akonadi
+{
+class Collection;
+
+namespace FileStore
+{
+class AbstractJobSession;
+
+/**
+ */
+class AKONADI_FILESTORE_EXPORT CollectionDeleteJob : public Job
+{
+    friend class AbstractJobSession;
+
+    Q_OBJECT
+
+public:
+    explicit CollectionDeleteJob(const Collection &collection, AbstractJobSession *session = Q_NULLPTR);
+
+    virtual ~CollectionDeleteJob();
+
+    Collection collection() const;
+
+    bool accept(Visitor *visitor) Q_DECL_OVERRIDE;
+
+private:
+    void handleCollectionDeleted(const Collection &collection);
+
+private:
+    class Private;
+    Private *const d;
+};
+
+}
+}
+
+#endif
+
diff --git a/resources/shared/filestore/collectionfetchjob.cpp b/resources/shared/filestore/collectionfetchjob.cpp
new file mode 100644 (file)
index 0000000..379c2c3
--- /dev/null
@@ -0,0 +1,95 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2009 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "collectionfetchjob.h"
+
+#include "session_p.h"
+
+#include <collectionfetchscope.h>
+
+using namespace Akonadi;
+
+class FileStore::CollectionFetchJob::Private
+{
+public:
+    Private() : mType(FileStore::CollectionFetchJob::Base)
+    {
+    }
+
+    FileStore::CollectionFetchJob::Type mType;
+    Collection mCollection;
+
+    CollectionFetchScope mFetchScope;
+
+    Collection::List mCollections;
+};
+
+FileStore::CollectionFetchJob::CollectionFetchJob(const Collection &collection, Type type, FileStore::AbstractJobSession *session)
+    : FileStore::Job(session), d(new Private())
+{
+    Q_ASSERT(session != 0);
+
+    d->mType = type;
+    d->mCollection = collection;
+
+    session->addJob(this);
+}
+
+FileStore::CollectionFetchJob::~CollectionFetchJob()
+{
+    delete d;
+}
+
+FileStore::CollectionFetchJob::Type FileStore::CollectionFetchJob::type() const
+{
+    return d->mType;
+}
+
+Collection FileStore::CollectionFetchJob::collection() const
+{
+    return d->mCollection;
+}
+
+void FileStore::CollectionFetchJob::setFetchScope(const CollectionFetchScope &fetchScope)
+{
+    d->mFetchScope = fetchScope;
+}
+
+CollectionFetchScope &FileStore::CollectionFetchJob::fetchScope()
+{
+    return d->mFetchScope;
+}
+
+Collection::List FileStore::CollectionFetchJob::collections() const
+{
+    return d->mCollections;
+}
+
+bool FileStore::CollectionFetchJob::accept(FileStore::Job::Visitor *visitor)
+{
+    return visitor->visit(this);
+}
+
+void FileStore::CollectionFetchJob::handleCollectionsReceived(const Collection::List &collections)
+{
+    d->mCollections << collections;
+
+    Q_EMIT collectionsReceived(collections);
+}
+
diff --git a/resources/shared/filestore/collectionfetchjob.h b/resources/shared/filestore/collectionfetchjob.h
new file mode 100644 (file)
index 0000000..3759803
--- /dev/null
@@ -0,0 +1,81 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2009,2010 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef AKONADI_FILESTORE_COLLECTIONFETCHJOB_H
+#define AKONADI_FILESTORE_COLLECTIONFETCHJOB_H
+
+#include "job.h"
+
+#include <collection.h>
+
+namespace Akonadi
+{
+class CollectionFetchScope;
+
+namespace FileStore
+{
+class AbstractJobSession;
+
+/**
+ */
+class AKONADI_FILESTORE_EXPORT CollectionFetchJob : public Job
+{
+    friend class AbstractJobSession;
+
+    Q_OBJECT
+
+public:
+    enum Type {
+        Base,
+        FirstLevel,
+        Recursive
+    };
+
+    explicit CollectionFetchJob(const Collection &collection, Type type = FirstLevel, AbstractJobSession *session = Q_NULLPTR);
+
+    virtual ~CollectionFetchJob();
+
+    Type type() const;
+
+    Collection collection() const;
+
+    void setFetchScope(const CollectionFetchScope &fetchScope);
+
+    CollectionFetchScope &fetchScope();
+
+    Collection::List collections() const;
+
+    bool accept(Visitor *visitor) Q_DECL_OVERRIDE;
+
+Q_SIGNALS:
+    void collectionsReceived(const Akonadi::Collection::List &items);
+
+private:
+    void handleCollectionsReceived(const Akonadi::Collection::List &collections);
+
+private:
+    class Private;
+    Private *const d;
+};
+
+}
+}
+
+#endif
+
diff --git a/resources/shared/filestore/collectionmodifyjob.cpp b/resources/shared/filestore/collectionmodifyjob.cpp
new file mode 100644 (file)
index 0000000..0decc26
--- /dev/null
@@ -0,0 +1,62 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "collectionmodifyjob.h"
+
+#include "session_p.h"
+
+using namespace Akonadi;
+
+class FileStore::CollectionModifyJob::Private
+{
+public:
+    Collection mCollection;
+};
+
+FileStore::CollectionModifyJob::CollectionModifyJob(const Collection &collection, FileStore::AbstractJobSession *session)
+    : FileStore::Job(session), d(new Private())
+{
+    Q_ASSERT(session != 0);
+
+    d->mCollection = collection;
+
+    session->addJob(this);
+}
+
+FileStore::CollectionModifyJob::~CollectionModifyJob()
+{
+    delete d;
+}
+
+Collection FileStore::CollectionModifyJob::collection() const
+{
+    return d->mCollection;
+}
+
+bool FileStore::CollectionModifyJob::accept(FileStore::Job::Visitor *visitor)
+{
+    return visitor->visit(this);
+}
+
+void FileStore::CollectionModifyJob::handleCollectionModified(const Collection &collection)
+{
+    d->mCollection = collection;
+}
+
diff --git a/resources/shared/filestore/collectionmodifyjob.h b/resources/shared/filestore/collectionmodifyjob.h
new file mode 100644 (file)
index 0000000..5869ab3
--- /dev/null
@@ -0,0 +1,63 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef AKONADI_FILESTORE_COLLECTIONMODIFYJOB_H
+#define AKONADI_FILESTORE_COLLECTIONMODIFYJOB_H
+
+#include "job.h"
+
+namespace Akonadi
+{
+class Collection;
+
+namespace FileStore
+{
+class AbstractJobSession;
+
+/**
+ */
+class AKONADI_FILESTORE_EXPORT CollectionModifyJob : public Job
+{
+    friend class AbstractJobSession;
+
+    Q_OBJECT
+
+public:
+    explicit CollectionModifyJob(const Collection &collection, AbstractJobSession *session = Q_NULLPTR);
+
+    virtual ~CollectionModifyJob();
+
+    Collection collection() const;
+
+    bool accept(Visitor *visitor) Q_DECL_OVERRIDE;
+
+private:
+    void handleCollectionModified(const Collection &collection);
+
+private:
+    class Private;
+    Private *const d;
+};
+
+}
+}
+
+#endif
+
diff --git a/resources/shared/filestore/collectionmovejob.cpp b/resources/shared/filestore/collectionmovejob.cpp
new file mode 100644 (file)
index 0000000..03f4f54
--- /dev/null
@@ -0,0 +1,69 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "collectionmovejob.h"
+
+#include "session_p.h"
+
+using namespace Akonadi;
+
+class FileStore::CollectionMoveJob::Private
+{
+public:
+    Collection mCollection;
+    Collection mTargetParent;
+};
+
+FileStore::CollectionMoveJob::CollectionMoveJob(const Collection &collection, const Collection &targetParent, FileStore::AbstractJobSession *session)
+    : FileStore::Job(session), d(new Private())
+{
+    Q_ASSERT(session != 0);
+
+    d->mCollection = collection;
+    d->mTargetParent = targetParent;
+
+    session->addJob(this);
+}
+
+FileStore::CollectionMoveJob::~CollectionMoveJob()
+{
+    delete d;
+}
+
+Collection FileStore::CollectionMoveJob::collection() const
+{
+    return d->mCollection;
+}
+
+Collection FileStore::CollectionMoveJob::targetParent() const
+{
+    return d->mTargetParent;
+}
+
+bool FileStore::CollectionMoveJob::accept(FileStore::Job::Visitor *visitor)
+{
+    return visitor->visit(this);
+}
+
+void FileStore::CollectionMoveJob::handleCollectionMoved(const Collection &collection)
+{
+    d->mCollection = collection;
+}
+
diff --git a/resources/shared/filestore/collectionmovejob.h b/resources/shared/filestore/collectionmovejob.h
new file mode 100644 (file)
index 0000000..d242fb8
--- /dev/null
@@ -0,0 +1,65 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef AKONADI_FILESTORE_COLLECTIONMOVEJOB_H
+#define AKONADI_FILESTORE_COLLECTIONMOVEJOB_H
+
+#include "job.h"
+
+namespace Akonadi
+{
+class Collection;
+
+namespace FileStore
+{
+class AbstractJobSession;
+
+/**
+ */
+class AKONADI_FILESTORE_EXPORT CollectionMoveJob : public Job
+{
+    friend class AbstractJobSession;
+
+    Q_OBJECT
+
+public:
+    explicit CollectionMoveJob(const Collection &collection, const Collection &targetParent, AbstractJobSession *session = Q_NULLPTR);
+
+    virtual ~CollectionMoveJob();
+
+    Collection collection() const;
+
+    Collection targetParent() const;
+
+    bool accept(Visitor *visitor) Q_DECL_OVERRIDE;
+
+private:
+    void handleCollectionMoved(const Collection &collection);
+
+private:
+    class Private;
+    Private *const d;
+};
+
+}
+}
+
+#endif
+
diff --git a/resources/shared/filestore/entitycompactchangeattribute.cpp b/resources/shared/filestore/entitycompactchangeattribute.cpp
new file mode 100644 (file)
index 0000000..4e5f65c
--- /dev/null
@@ -0,0 +1,106 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "entitycompactchangeattribute.h"
+
+#include <QDataStream>
+
+using namespace Akonadi;
+
+class FileStore::EntityCompactChangeAttribute::Private
+{
+public:
+    Private &operator=(const Private &other)
+    {
+        if (&other == this) {
+            return *this;
+        }
+
+        mRemoteId  = other.mRemoteId;
+        mRemoteRev = other.mRemoteRev;
+        return *this;
+    }
+
+public:
+    QString mRemoteId;
+    QString mRemoteRev;
+};
+
+FileStore::EntityCompactChangeAttribute::EntityCompactChangeAttribute()
+    : Attribute(), d(new Private())
+{
+}
+
+FileStore::EntityCompactChangeAttribute::~EntityCompactChangeAttribute()
+{
+    delete d;
+}
+
+void FileStore::EntityCompactChangeAttribute::setRemoteId(const QString &remoteId)
+{
+    d->mRemoteId = remoteId;
+}
+
+QString FileStore::EntityCompactChangeAttribute::remoteId() const
+{
+    return d->mRemoteId;
+}
+
+void FileStore::EntityCompactChangeAttribute::setRemoteRevision(const QString &remoteRev)
+{
+    d->mRemoteRev = remoteRev;
+}
+
+QString FileStore::EntityCompactChangeAttribute::remoteRevision() const
+{
+    return d->mRemoteRev;
+}
+
+QByteArray FileStore::EntityCompactChangeAttribute::type() const
+{
+    static const QByteArray sType("ENTITYCOMPACTCHANGE");
+    return sType;
+}
+
+FileStore::EntityCompactChangeAttribute *FileStore::EntityCompactChangeAttribute::clone() const
+{
+    FileStore::EntityCompactChangeAttribute *copy = new FileStore::EntityCompactChangeAttribute();
+    *(copy->d) = *d;
+    return copy;
+}
+
+QByteArray FileStore::EntityCompactChangeAttribute::serialized() const
+{
+    QByteArray data;
+    QDataStream stream(&data, QIODevice::WriteOnly);
+
+    stream << d->mRemoteId;
+    stream << d->mRemoteRev;
+
+    return data;
+}
+
+void FileStore::EntityCompactChangeAttribute::deserialize(const QByteArray &data)
+{
+    QDataStream stream(data);
+    stream >> d->mRemoteId;
+    stream >> d->mRemoteRev;
+}
+
diff --git a/resources/shared/filestore/entitycompactchangeattribute.h b/resources/shared/filestore/entitycompactchangeattribute.h
new file mode 100644 (file)
index 0000000..b2b5406
--- /dev/null
@@ -0,0 +1,70 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef AKONADI_FILESTORE_ENTITYCOMPACTCHANGEATTRIBUTE_H
+#define AKONADI_FILESTORE_ENTITYCOMPACTCHANGEATTRIBUTE_H
+
+#include "akonadi-filestore_export.h"
+
+#include <Attribute>
+
+namespace Akonadi
+{
+
+namespace FileStore
+{
+
+class AKONADI_FILESTORE_EXPORT EntityCompactChangeAttribute : public Attribute
+{
+public:
+    EntityCompactChangeAttribute();
+
+    ~EntityCompactChangeAttribute();
+
+    void setRemoteId(const QString &remoteId);
+
+    QString remoteId() const;
+
+    void setRemoteRevision(const QString &remoteRev);
+
+    QString remoteRevision() const;
+
+public:
+    QByteArray type() const Q_DECL_OVERRIDE;
+
+    EntityCompactChangeAttribute *clone() const Q_DECL_OVERRIDE;
+
+    QByteArray serialized() const Q_DECL_OVERRIDE;
+
+    void deserialize(const QByteArray &data) Q_DECL_OVERRIDE;
+
+private:
+    //@cond PRIVATE
+    class Private;
+    Private *const d;
+    //@endcond
+};
+
+}
+
+}
+
+#endif
+
diff --git a/resources/shared/filestore/itemcreatejob.cpp b/resources/shared/filestore/itemcreatejob.cpp
new file mode 100644 (file)
index 0000000..c050f28
--- /dev/null
@@ -0,0 +1,66 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2009 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "itemcreatejob.h"
+
+#include "session_p.h"
+
+using namespace Akonadi;
+
+class FileStore::ItemCreateJob::Private
+{
+public:
+    Item mItem;
+    Collection mCollection;
+};
+
+FileStore::ItemCreateJob::ItemCreateJob(const Item &item, const Collection &collection, FileStore::AbstractJobSession *session)
+    : FileStore::Job(session), d(new Private())
+{
+    d->mItem = item;
+    d->mCollection = collection;
+
+    session->addJob(this);
+}
+
+FileStore::ItemCreateJob::~ItemCreateJob()
+{
+    delete d;
+}
+
+Collection FileStore::ItemCreateJob::collection() const
+{
+    return d->mCollection;
+}
+
+Item FileStore::ItemCreateJob::item() const
+{
+    return d->mItem;
+}
+
+bool FileStore::ItemCreateJob::accept(FileStore::Job::Visitor *visitor)
+{
+    return visitor->visit(this);
+}
+
+void FileStore::ItemCreateJob::handleItemCreated(const Item &item)
+{
+    d->mItem = item;
+}
+
diff --git a/resources/shared/filestore/itemcreatejob.h b/resources/shared/filestore/itemcreatejob.h
new file mode 100644 (file)
index 0000000..9587238
--- /dev/null
@@ -0,0 +1,65 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2009,2010 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef AKONADI_FILESTORE_ITEMCREATEJOB_H
+#define AKONADI_FILESTORE_ITEMCREATEJOB_H
+
+#include "job.h"
+
+namespace Akonadi
+{
+class Collection;
+class Item;
+
+namespace FileStore
+{
+class AbstractJobSession;
+
+/**
+ */
+class AKONADI_FILESTORE_EXPORT ItemCreateJob : public Job
+{
+    friend class AbstractJobSession;
+
+    Q_OBJECT
+
+public:
+    explicit ItemCreateJob(const Item &item, const Collection &collection, AbstractJobSession *session = Q_NULLPTR);
+
+    virtual ~ItemCreateJob();
+
+    Collection collection() const;
+
+    Item item() const;
+
+    bool accept(Visitor *visitor) Q_DECL_OVERRIDE;
+
+private:
+    void handleItemCreated(const Akonadi::Item &item);
+
+private:
+    class Private;
+    Private *const d;
+};
+
+}
+}
+
+#endif
+
diff --git a/resources/shared/filestore/itemdeletejob.cpp b/resources/shared/filestore/itemdeletejob.cpp
new file mode 100644 (file)
index 0000000..77ab175
--- /dev/null
@@ -0,0 +1,59 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2009 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "itemdeletejob.h"
+
+#include "session_p.h"
+
+using namespace Akonadi;
+
+class FileStore::ItemDeleteJob::Private
+{
+public:
+    Item mItem;
+};
+
+FileStore::ItemDeleteJob::ItemDeleteJob(const Item &item, FileStore::AbstractJobSession *session)
+    : FileStore::Job(session), d(new Private())
+{
+    d->mItem = item;
+
+    session->addJob(this);
+}
+
+FileStore::ItemDeleteJob::~ItemDeleteJob()
+{
+    delete d;
+}
+
+Item FileStore::ItemDeleteJob::item() const
+{
+    return d->mItem;
+}
+
+bool FileStore::ItemDeleteJob::accept(FileStore::Job::Visitor *visitor)
+{
+    return visitor->visit(this);
+}
+
+void FileStore::ItemDeleteJob::handleItemDeleted(const Item &item)
+{
+    d->mItem = item;
+}
+
diff --git a/resources/shared/filestore/itemdeletejob.h b/resources/shared/filestore/itemdeletejob.h
new file mode 100644 (file)
index 0000000..885f2f7
--- /dev/null
@@ -0,0 +1,62 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2009,2010 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef AKONADI_FILESTORE_ITEMDELETEJOB_H
+#define AKONADI_FILESTORE_ITEMDELETEJOB_H
+
+#include "job.h"
+
+namespace Akonadi
+{
+class Item;
+
+namespace FileStore
+{
+class AbstractJobSession;
+
+/**
+ */
+class AKONADI_FILESTORE_EXPORT ItemDeleteJob : public Job
+{
+    friend class AbstractJobSession;
+
+    Q_OBJECT
+
+public:
+    explicit ItemDeleteJob(const Item &item, AbstractJobSession *session = Q_NULLPTR);
+
+    virtual ~ItemDeleteJob();
+
+    Item item() const;
+
+    bool accept(Visitor *visitor) Q_DECL_OVERRIDE;
+
+private:
+    void handleItemDeleted(const Akonadi::Item &item);
+
+private:
+    class Private;
+    Private *const d;
+};
+
+}
+}
+
+#endif
+
diff --git a/resources/shared/filestore/itemfetchjob.cpp b/resources/shared/filestore/itemfetchjob.cpp
new file mode 100644 (file)
index 0000000..d5bf24d
--- /dev/null
@@ -0,0 +1,96 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2009 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "itemfetchjob.h"
+
+#include "session_p.h"
+
+#include <itemfetchscope.h>
+
+using namespace Akonadi;
+
+class FileStore::ItemFetchJob::Private
+{
+public:
+    ItemFetchScope mFetchScope;
+
+    Item::List mItems;
+
+    Collection mCollection;
+    Item mItem;
+};
+
+FileStore::ItemFetchJob::ItemFetchJob(const Collection &collection, FileStore::AbstractJobSession *session)
+    : FileStore::Job(session), d(new Private())
+{
+    d->mCollection = collection;
+
+    session->addJob(this);
+}
+
+FileStore::ItemFetchJob::ItemFetchJob(const Item &item, FileStore::AbstractJobSession *session)
+    : FileStore::Job(session), d(new Private())
+{
+    d->mItem = item;
+
+    session->addJob(this);
+}
+
+FileStore::ItemFetchJob::~ItemFetchJob()
+{
+    delete d;
+}
+
+Collection FileStore::ItemFetchJob::collection() const
+{
+    return d->mCollection;
+}
+
+Item FileStore::ItemFetchJob::item() const
+{
+    return d->mItem;
+}
+
+void FileStore::ItemFetchJob::setFetchScope(const ItemFetchScope &fetchScope)
+{
+    d->mFetchScope = fetchScope;
+}
+
+ItemFetchScope &FileStore::ItemFetchJob::fetchScope()
+{
+    return d->mFetchScope;
+}
+
+Item::List FileStore::ItemFetchJob::items() const
+{
+    return d->mItems;
+}
+
+bool FileStore::ItemFetchJob::accept(FileStore::Job::Visitor *visitor)
+{
+    return visitor->visit(this);
+}
+
+void FileStore::ItemFetchJob::handleItemsReceived(const Item::List &items)
+{
+    d->mItems << items;
+
+    Q_EMIT itemsReceived(items);
+}
+
diff --git a/resources/shared/filestore/itemfetchjob.h b/resources/shared/filestore/itemfetchjob.h
new file mode 100644 (file)
index 0000000..aa9ea32
--- /dev/null
@@ -0,0 +1,78 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2009,2010 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef AKONADI_FILESTORE_ITEMFETCHJOB_H
+#define AKONADI_FILESTORE_ITEMFETCHJOB_H
+
+#include "job.h"
+
+#include <item.h>
+
+namespace Akonadi
+{
+class Collection;
+class ItemFetchScope;
+
+namespace FileStore
+{
+class AbstractJobSession;
+
+/**
+ */
+class AKONADI_FILESTORE_EXPORT ItemFetchJob : public Job
+{
+    friend class AbstractJobSession;
+
+    Q_OBJECT
+
+public:
+    explicit ItemFetchJob(const Collection &collection, AbstractJobSession *session = Q_NULLPTR);
+
+    explicit ItemFetchJob(const Item &item, AbstractJobSession *session = Q_NULLPTR);
+
+    virtual ~ItemFetchJob();
+
+    Collection collection() const;
+
+    Item item() const;
+
+    void setFetchScope(const ItemFetchScope &fetchScope);
+
+    ItemFetchScope &fetchScope();
+
+    Item::List items() const;
+
+    bool accept(Visitor *visitor) Q_DECL_OVERRIDE;
+
+Q_SIGNALS:
+    void itemsReceived(const Akonadi::Item::List &items);
+
+private:
+    void handleItemsReceived(const Akonadi::Item::List &items);
+
+private:
+    class Private;
+    Private *const d;
+};
+
+}
+}
+
+#endif
+
diff --git a/resources/shared/filestore/itemmodifyjob.cpp b/resources/shared/filestore/itemmodifyjob.cpp
new file mode 100644 (file)
index 0000000..cc4d982
--- /dev/null
@@ -0,0 +1,85 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2009 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "itemmodifyjob.h"
+
+#include "session_p.h"
+
+using namespace Akonadi;
+
+class FileStore::ItemModifyJob::Private
+{
+public:
+    Private() : mIgnorePayload(false)
+    {
+    }
+
+    bool mIgnorePayload;
+    Item mItem;
+    QSet<QByteArray> mParts;
+};
+
+FileStore::ItemModifyJob::ItemModifyJob(const Item &item, FileStore::AbstractJobSession *session)
+    : FileStore::Job(session), d(new Private())
+{
+    d->mItem = item;
+
+    session->addJob(this);
+}
+
+FileStore::ItemModifyJob::~ItemModifyJob()
+{
+    delete d;
+}
+
+void FileStore::ItemModifyJob::setIgnorePayload(bool ignorePayload)
+{
+    d->mIgnorePayload = ignorePayload;
+}
+
+bool FileStore::ItemModifyJob::ignorePayload() const
+{
+    return d->mIgnorePayload;
+}
+
+Item FileStore::ItemModifyJob::item() const
+{
+    return d->mItem;
+}
+
+void FileStore::ItemModifyJob::setParts(const QSet<QByteArray> &parts)
+{
+    d->mParts = parts;
+}
+
+const QSet<QByteArray> &FileStore::ItemModifyJob::parts() const
+{
+    return d->mParts;
+}
+
+bool FileStore::ItemModifyJob::accept(FileStore::Job::Visitor *visitor)
+{
+    return visitor->visit(this);
+}
+
+void FileStore::ItemModifyJob::handleItemModified(const Item &item)
+{
+    d->mItem = item;
+}
+
diff --git a/resources/shared/filestore/itemmodifyjob.h b/resources/shared/filestore/itemmodifyjob.h
new file mode 100644 (file)
index 0000000..8e5bf6d
--- /dev/null
@@ -0,0 +1,70 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2009,2010 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef AKONADI_FILESTORE_ITEMMODIFYJOB_H
+#define AKONADI_FILESTORE_ITEMMODIFYJOB_H
+
+#include "job.h"
+
+#include <item.h>
+
+namespace Akonadi
+{
+
+namespace FileStore
+{
+class AbstractJobSession;
+
+/**
+ */
+class AKONADI_FILESTORE_EXPORT ItemModifyJob : public Job
+{
+    friend class AbstractJobSession;
+
+    Q_OBJECT
+
+public:
+    explicit ItemModifyJob(const Item &item, AbstractJobSession *session = Q_NULLPTR);
+
+    virtual ~ItemModifyJob();
+
+    void setIgnorePayload(bool ignorePayload);
+
+    bool ignorePayload() const;
+
+    Item item() const;
+
+    const QSet<QByteArray> &parts() const;
+    void setParts(const QSet<QByteArray> &parts);
+
+    bool accept(Visitor *visitor) Q_DECL_OVERRIDE;
+
+private:
+    void handleItemModified(const Akonadi::Item &item);
+
+private:
+    class Private;
+    Private *const d;
+};
+
+}
+}
+
+#endif
+
diff --git a/resources/shared/filestore/itemmovejob.cpp b/resources/shared/filestore/itemmovejob.cpp
new file mode 100644 (file)
index 0000000..916fe8e
--- /dev/null
@@ -0,0 +1,67 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "itemmovejob.h"
+
+#include "session_p.h"
+
+using namespace Akonadi;
+
+class FileStore::ItemMoveJob::Private
+{
+public:
+    Item mItem;
+    Collection mTargetParent;
+};
+
+FileStore::ItemMoveJob::ItemMoveJob(const Item &item, const Collection &targetParent, FileStore::AbstractJobSession *session)
+    : FileStore::Job(session), d(new Private())
+{
+    d->mItem = item;
+    d->mTargetParent = targetParent;
+
+    session->addJob(this);
+}
+
+FileStore::ItemMoveJob::~ItemMoveJob()
+{
+    delete d;
+}
+
+Collection FileStore::ItemMoveJob::targetParent() const
+{
+    return d->mTargetParent;
+}
+
+Item FileStore::ItemMoveJob::item() const
+{
+    return d->mItem;
+}
+
+bool FileStore::ItemMoveJob::accept(FileStore::Job::Visitor *visitor)
+{
+    return visitor->visit(this);
+}
+
+void FileStore::ItemMoveJob::handleItemMoved(const Item &item)
+{
+    d->mItem = item;
+}
+
diff --git a/resources/shared/filestore/itemmovejob.h b/resources/shared/filestore/itemmovejob.h
new file mode 100644 (file)
index 0000000..8dc6e8a
--- /dev/null
@@ -0,0 +1,66 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef AKONADI_FILESTORE_ITEMMOVEJOB_H
+#define AKONADI_FILESTORE_ITEMMOVEJOB_H
+
+#include "job.h"
+
+namespace Akonadi
+{
+class Collection;
+class Item;
+
+namespace FileStore
+{
+class AbstractJobSession;
+
+/**
+ */
+class AKONADI_FILESTORE_EXPORT ItemMoveJob : public Job
+{
+    friend class AbstractJobSession;
+
+    Q_OBJECT
+
+public:
+    ItemMoveJob(const Item &item, const Collection &targetParent, AbstractJobSession *session = Q_NULLPTR);
+
+    virtual ~ItemMoveJob();
+
+    Collection targetParent() const;
+
+    Item item() const;
+
+    bool accept(Visitor *visitor) Q_DECL_OVERRIDE;
+
+private:
+    void handleItemMoved(const Item &item);
+
+private:
+    class Private;
+    Private *const d;
+};
+
+}
+}
+
+#endif
+
diff --git a/resources/shared/filestore/job.cpp b/resources/shared/filestore/job.cpp
new file mode 100644 (file)
index 0000000..cfbd37c
--- /dev/null
@@ -0,0 +1,50 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2009 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "job.h"
+
+#include "session_p.h"
+
+using namespace Akonadi;
+
+class FileStore::Job::Private
+{
+public:
+};
+
+FileStore::Job::Job(FileStore::AbstractJobSession *session)
+    : KJob(session), d(Q_NULLPTR/*new Private(this)*/) // nullptr until it's needed
+{
+    setAutoDelete(true);
+}
+
+FileStore::Job::~Job()
+{
+    delete d;
+}
+
+void FileStore::Job::start()
+{
+}
+
+bool FileStore::Job::accept(FileStore::Job::Visitor *visitor)
+{
+    return visitor->visit(this);
+}
+
diff --git a/resources/shared/filestore/job.h b/resources/shared/filestore/job.h
new file mode 100644 (file)
index 0000000..569545e
--- /dev/null
@@ -0,0 +1,108 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2009 Kevin Krammer <kevin.krammer@gmx.at>
+    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
+    Author: Kevin Krammer, krake@kdab.com
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef AKONADI_FILESTORE_JOB_H
+#define AKONADI_FILESTORE_JOB_H
+
+#include "akonadi-filestore_export.h"
+
+#include <KJob>
+
+namespace Akonadi
+{
+
+namespace FileStore
+{
+class AbstractJobSession;
+class CollectionCreateJob;
+class CollectionDeleteJob;
+class CollectionFetchJob;
+class CollectionModifyJob;
+class CollectionMoveJob;
+class ItemCreateJob;
+class ItemDeleteJob;
+class ItemFetchJob;
+class ItemModifyJob;
+class ItemMoveJob;
+class StoreCompactJob;
+
+/**
+ */
+class AKONADI_FILESTORE_EXPORT Job : public KJob
+{
+    friend class AbstractJobSession;
+
+    Q_OBJECT
+
+public:
+    class Visitor
+    {
+    public:
+        virtual ~Visitor() {}
+
+        virtual bool visit(Job *job) = 0;
+
+        virtual bool visit(CollectionCreateJob *job) = 0;
+
+        virtual bool visit(CollectionDeleteJob *job) = 0;
+
+        virtual bool visit(CollectionFetchJob *job) = 0;
+
+        virtual bool visit(CollectionModifyJob *job) = 0;
+
+        virtual bool visit(CollectionMoveJob *job) = 0;
+
+        virtual bool visit(ItemCreateJob *job) = 0;
+
+        virtual bool visit(ItemDeleteJob *job) = 0;
+
+        virtual bool visit(ItemFetchJob *job) = 0;
+
+        virtual bool visit(ItemModifyJob *job) = 0;
+
+        virtual bool visit(ItemMoveJob *job) = 0;
+
+        virtual bool visit(StoreCompactJob *job) = 0;
+    };
+
+    enum ErrorCodes {
+        InvalidStoreState = KJob::UserDefinedError + 1,
+        InvalidJobContext
+    };
+
+    explicit Job(AbstractJobSession *session = Q_NULLPTR);
+
+    virtual ~Job();
+
+    void start() Q_DECL_OVERRIDE;
+
+    virtual bool accept(Visitor *visitor);
+
+private:
+    class Private;
+    Private *d;
+};
+
+}
+}
+
+#endif
+
diff --git a/resources/shared/filestore/session.cpp b/resources/shared/filestore/session.cpp
new file mode 100644 (file)
index 0000000..07f4f8b
--- /dev/null
@@ -0,0 +1,144 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2009 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "session_p.h"
+
+#include "collectioncreatejob.h"
+#include "collectiondeletejob.h"
+#include "collectionfetchjob.h"
+#include "collectionmodifyjob.h"
+#include "collectionmovejob.h"
+#include "itemcreatejob.h"
+#include "itemfetchjob.h"
+#include "itemmodifyjob.h"
+#include "itemmovejob.h"
+#include "storecompactjob.h"
+
+using namespace Akonadi;
+
+FileStore::AbstractJobSession::AbstractJobSession(QObject *parent)
+    : QObject(parent)
+{
+}
+
+FileStore::AbstractJobSession::~AbstractJobSession()
+{
+}
+
+void FileStore::AbstractJobSession::notifyCollectionsReceived(FileStore::Job *job, const Collection::List &collections)
+{
+    FileStore::CollectionFetchJob *fetchJob = dynamic_cast<FileStore::CollectionFetchJob *>(job);
+    if (fetchJob != Q_NULLPTR) {
+        fetchJob->handleCollectionsReceived(collections);
+    }
+}
+
+void FileStore::AbstractJobSession::notifyCollectionCreated(FileStore::Job *job, const Collection &collection)
+{
+    FileStore::CollectionCreateJob *createJob = dynamic_cast<FileStore::CollectionCreateJob *>(job);
+    if (createJob != Q_NULLPTR) {
+        createJob->handleCollectionCreated(collection);
+    }
+}
+
+void FileStore::AbstractJobSession::notifyCollectionDeleted(FileStore::Job *job, const Collection &collection)
+{
+    FileStore::CollectionDeleteJob *deleteJob = dynamic_cast<FileStore::CollectionDeleteJob *>(job);
+    if (deleteJob != Q_NULLPTR) {
+        deleteJob->handleCollectionDeleted(collection);
+    }
+}
+
+void FileStore::AbstractJobSession::notifyCollectionModified(FileStore::Job *job, const Collection &collection)
+{
+    FileStore::CollectionModifyJob *modifyJob = dynamic_cast<FileStore::CollectionModifyJob *>(job);
+    if (modifyJob != Q_NULLPTR) {
+        modifyJob->handleCollectionModified(collection);
+    }
+}
+
+void FileStore::AbstractJobSession::notifyCollectionMoved(FileStore::Job *job, const Collection &collection)
+{
+    FileStore::CollectionMoveJob *moveJob = dynamic_cast<FileStore::CollectionMoveJob *>(job);
+    if (moveJob != Q_NULLPTR) {
+        moveJob->handleCollectionMoved(collection);
+    }
+}
+
+void FileStore::AbstractJobSession::notifyItemsReceived(FileStore::Job *job, const Item::List &items)
+{
+    FileStore::ItemFetchJob *fetchJob = dynamic_cast<FileStore::ItemFetchJob *>(job);
+    if (fetchJob != Q_NULLPTR) {
+        fetchJob->handleItemsReceived(items);
+    }
+}
+
+void FileStore::AbstractJobSession::notifyItemCreated(FileStore::Job *job, const Item &item)
+{
+    FileStore::ItemCreateJob *createJob = dynamic_cast<FileStore::ItemCreateJob *>(job);
+    if (createJob != Q_NULLPTR) {
+        createJob->handleItemCreated(item);
+    }
+}
+
+void FileStore::AbstractJobSession::notifyItemModified(FileStore::Job *job, const Item &item)
+{
+    FileStore::ItemModifyJob *modifyJob = dynamic_cast<FileStore::ItemModifyJob *>(job);
+    if (modifyJob != Q_NULLPTR) {
+        modifyJob->handleItemModified(item);
+    }
+}
+
+void FileStore::AbstractJobSession::notifyItemMoved(FileStore::Job *job, const Item &item)
+{
+    FileStore::ItemMoveJob *moveJob = dynamic_cast<FileStore::ItemMoveJob *>(job);
+    if (moveJob != Q_NULLPTR) {
+        moveJob->handleItemMoved(item);
+    }
+}
+
+void FileStore::AbstractJobSession::notifyCollectionsChanged(FileStore::Job *job, const Collection::List &collections)
+{
+    FileStore::StoreCompactJob *compactJob = dynamic_cast<FileStore::StoreCompactJob *>(job);
+    if (compactJob != Q_NULLPTR) {
+        compactJob->handleCollectionsChanged(collections);
+    }
+}
+
+void FileStore::AbstractJobSession::notifyItemsChanged(FileStore::Job *job, const Item::List &items)
+{
+    FileStore::StoreCompactJob *compactJob = dynamic_cast<FileStore::StoreCompactJob *>(job);
+    if (compactJob != Q_NULLPTR) {
+        compactJob->handleItemsChanged(items);
+    }
+}
+
+void FileStore::AbstractJobSession::setError(FileStore::Job *job, int errorCode, const QString &errorText)
+{
+    job->setError(errorCode);
+    job->setErrorText(errorText);
+}
+
+void FileStore::AbstractJobSession::emitResult(FileStore::Job *job)
+{
+    removeJob(job);
+
+    job->emitResult();
+}
+
diff --git a/resources/shared/filestore/session_p.h b/resources/shared/filestore/session_p.h
new file mode 100644 (file)
index 0000000..b145ff1
--- /dev/null
@@ -0,0 +1,88 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2009 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef AKONADI_FILESTORE_SESSION_P_H
+#define AKONADI_FILESTORE_SESSION_P_H
+
+#include <collection.h>
+#include <item.h>
+
+#include <QObject>
+
+namespace Akonadi
+{
+
+namespace FileStore
+{
+
+class Job;
+
+/**
+ */
+class AbstractJobSession : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit AbstractJobSession(QObject *parent = Q_NULLPTR);
+
+    virtual ~AbstractJobSession();
+
+    virtual void addJob(Job *job) = 0;
+
+    virtual void cancelAllJobs() = 0;
+
+    void notifyCollectionsReceived(Job *job, const Collection::List &collections);
+
+    void notifyCollectionCreated(Job *job, const Collection &collection);
+
+    void notifyCollectionDeleted(Job *job, const Collection &collection);
+
+    void notifyCollectionModified(Job *job, const Collection &collection);
+
+    void notifyCollectionMoved(Job *job, const Collection &collection);
+
+    void notifyItemsReceived(Job *job, const Item::List &items);
+
+    void notifyItemCreated(Job *job, const Item &item);
+
+    void notifyItemModified(Job *job, const Item &item);
+
+    void notifyItemMoved(Job *job, const Item &item);
+
+    void notifyCollectionsChanged(Job *job, const Collection::List &collections);
+
+    void notifyItemsChanged(Job *job, const Item::List &items);
+
+    void setError(Job *job, int errorCode, const QString &errorText);
+
+    void emitResult(Job *job);
+
+Q_SIGNALS:
+    void jobsReady(const QList<FileStore::Job *> &jobs);
+
+protected:
+    virtual void removeJob(Job *job) = 0;
+};
+
+}
+}
+
+#endif
+
diff --git a/resources/shared/filestore/sessionimpls.cpp b/resources/shared/filestore/sessionimpls.cpp
new file mode 100644 (file)
index 0000000..470415b
--- /dev/null
@@ -0,0 +1,201 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2009 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "sessionimpls_p.h"
+
+#include "collectioncreatejob.h"
+#include "collectiondeletejob.h"
+#include "collectionfetchjob.h"
+#include "collectionmodifyjob.h"
+#include "collectionmovejob.h"
+#include "itemcreatejob.h"
+#include "itemdeletejob.h"
+#include "itemfetchjob.h"
+#include "itemmodifyjob.h"
+#include "itemmovejob.h"
+#include "storecompactjob.h"
+
+#include <QDebug>
+
+#include <QQueue>
+#include <QTimer>
+
+using namespace Akonadi;
+
+class AbstractEnqueueVisitor : public FileStore::Job::Visitor
+{
+public:
+    bool visit(FileStore::Job *job) Q_DECL_OVERRIDE {
+        enqueue(job, "Job");
+        return true;
+    }
+
+    bool visit(FileStore::CollectionCreateJob *job) Q_DECL_OVERRIDE {
+        enqueue(job, "CollectionCreateJob");
+        return true;
+    }
+
+    bool visit(FileStore::CollectionDeleteJob *job) Q_DECL_OVERRIDE {
+        enqueue(job, "CollectionDeleteJob");
+        return true;
+    }
+
+    bool visit(FileStore::CollectionFetchJob *job) Q_DECL_OVERRIDE {
+        enqueue(job, "CollectionFetchJob");
+        return true;
+    }
+
+    bool visit(FileStore::CollectionModifyJob *job) Q_DECL_OVERRIDE {
+        enqueue(job, "CollectionModifyJob");
+        return true;
+    }
+
+    bool visit(FileStore::CollectionMoveJob *job) Q_DECL_OVERRIDE {
+        enqueue(job, "CollectionMoveJob");
+        return true;
+    }
+
+    bool visit(FileStore::ItemCreateJob *job) Q_DECL_OVERRIDE {
+        enqueue(job, "ItemCreateJob");
+        return true;
+    }
+
+    bool visit(FileStore::ItemDeleteJob *job) Q_DECL_OVERRIDE {
+        enqueue(job, "ItemDeleteJob");
+        return true;
+    }
+
+    bool visit(FileStore::ItemFetchJob *job) Q_DECL_OVERRIDE {
+        enqueue(job, "ItemFetchJob");
+        return true;
+    }
+
+    bool visit(FileStore::ItemModifyJob *job) Q_DECL_OVERRIDE {
+        enqueue(job, "ItemModifyJob");
+        return true;
+    }
+
+    bool visit(FileStore::ItemMoveJob *job) Q_DECL_OVERRIDE {
+        enqueue(job, "ItemMoveJob");
+        return true;
+    }
+
+    bool visit(FileStore::StoreCompactJob *job) Q_DECL_OVERRIDE {
+        enqueue(job, "StoreCompactJob");
+        return true;
+    }
+
+protected:
+    virtual void enqueue(FileStore::Job *job, const char *className) = 0;
+};
+
+class FileStore::FiFoQueueJobSession::Private : public AbstractEnqueueVisitor
+{
+public:
+    explicit Private(FileStore::FiFoQueueJobSession *parent)
+        : mParent(parent)
+    {
+        QObject::connect(&mJobRunTimer, SIGNAL(timeout()), mParent, SLOT(runNextJob()));
+    }
+
+    void runNextJob()
+    {
+        /*      qDebug() << "Queue with" << mJobQueue.count() << "entries";*/
+        if (mJobQueue.isEmpty()) {
+            mJobRunTimer.stop();
+            return;
+        }
+
+        FileStore::Job *job = mJobQueue.dequeue();
+        while (job != Q_NULLPTR && job->error() != 0) {
+            /*        qDebug() << "Dequeued job" << job << "has error ("
+                             << job->error() << "," << job->errorText() << ")";*/
+            mParent->emitResult(job);
+            if (!mJobQueue.isEmpty()) {
+                job = mJobQueue.dequeue();
+            } else {
+                job = Q_NULLPTR;
+            }
+        }
+
+        if (job != Q_NULLPTR) {
+            /*        qDebug() << "Dequeued job" << job << "is ready";*/
+            QList<FileStore::Job *> jobs;
+            jobs << job;
+
+            Q_EMIT mParent->jobsReady(jobs);
+        } else {
+            /*        qDebug() << "Queue now empty";*/
+            mJobRunTimer.stop();
+        }
+    }
+
+public:
+    QQueue<FileStore::Job *> mJobQueue;
+
+    QTimer mJobRunTimer;
+
+protected:
+    void enqueue(FileStore::Job *job, const char *className) Q_DECL_OVERRIDE {
+        Q_UNUSED(className);
+        mJobQueue.enqueue(job);
+
+//       qDebug() << "adding" << className << ". Queue now with"
+//                << mJobQueue.count() << "entries";
+
+        mJobRunTimer.start(0);
+    }
+
+private:
+    FileStore::FiFoQueueJobSession *mParent;
+};
+
+FileStore::FiFoQueueJobSession::FiFoQueueJobSession(QObject *parent)
+    : FileStore::AbstractJobSession(parent), d(new Private(this))
+{
+}
+
+FileStore::FiFoQueueJobSession::~FiFoQueueJobSession()
+{
+    cancelAllJobs();
+    delete d;
+}
+
+void FileStore::FiFoQueueJobSession::addJob(FileStore::Job *job)
+{
+    job->accept(d);
+}
+
+void FileStore::FiFoQueueJobSession::cancelAllJobs()
+{
+    // KJob::kill() also deletes the job
+    foreach (FileStore::Job *job, d->mJobQueue) {
+        job->kill(KJob::EmitResult);
+    }
+
+    d->mJobQueue.clear();
+}
+
+void FileStore::FiFoQueueJobSession::removeJob(FileStore::Job *job)
+{
+    d->mJobQueue.removeAll(job);
+}
+
+#include "moc_sessionimpls_p.cpp"
+
diff --git a/resources/shared/filestore/sessionimpls_p.h b/resources/shared/filestore/sessionimpls_p.h
new file mode 100644 (file)
index 0000000..7951d3f
--- /dev/null
@@ -0,0 +1,60 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2009,2010 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef AKONADI_FILESTORE_SESSIONIMPLS_P_H
+#define AKONADI_FILESTORE_SESSIONIMPLS_P_H
+
+#include "session_p.h"
+
+namespace Akonadi
+{
+
+namespace FileStore
+{
+
+/**
+ */
+class FiFoQueueJobSession : public AbstractJobSession
+{
+    Q_OBJECT
+
+public:
+    explicit FiFoQueueJobSession(QObject *parent = Q_NULLPTR);
+
+    virtual ~FiFoQueueJobSession();
+
+    void addJob(Job *job) Q_DECL_OVERRIDE;
+
+    void cancelAllJobs() Q_DECL_OVERRIDE;
+
+protected:
+    void removeJob(Job *job) Q_DECL_OVERRIDE;
+
+private:
+    class Private;
+    Private *const d;
+
+    Q_PRIVATE_SLOT(d, void runNextJob())
+};
+
+}
+}
+
+#endif
+
diff --git a/resources/shared/filestore/storecompactjob.cpp b/resources/shared/filestore/storecompactjob.cpp
new file mode 100644 (file)
index 0000000..3b7e29b
--- /dev/null
@@ -0,0 +1,78 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2009 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#include "storecompactjob.h"
+
+#include "session_p.h"
+
+using namespace Akonadi;
+
+class FileStore::StoreCompactJob::Private
+{
+public:
+    explicit Private(FileStore::StoreCompactJob *parent)
+        : mParent(parent)
+    {
+    }
+
+public:
+    FileStore::StoreCompactJob *mParent;
+
+    Collection::List mCollections;
+    Item::List mItems;
+};
+
+FileStore::StoreCompactJob::StoreCompactJob(FileStore::AbstractJobSession *session)
+    : FileStore::Job(session), d(new Private(this))
+{
+    session->addJob(this);
+}
+
+FileStore::StoreCompactJob::~StoreCompactJob()
+{
+    delete d;
+}
+
+bool FileStore::StoreCompactJob::accept(FileStore::Job::Visitor *visitor)
+{
+    return visitor->visit(this);
+}
+
+Item::List FileStore::StoreCompactJob::changedItems() const
+{
+    return d->mItems;
+}
+
+Collection::List FileStore::StoreCompactJob::changedCollections() const
+{
+    return d->mCollections;
+}
+
+void FileStore::StoreCompactJob::handleCollectionsChanged(const Collection::List &collections)
+{
+    d->mCollections << collections;
+    Q_EMIT collectionsChanged(collections);
+}
+
+void FileStore::StoreCompactJob::handleItemsChanged(const Item::List &items)
+{
+    d->mItems << items;
+    Q_EMIT itemsChanged(items);
+}
+
diff --git a/resources/shared/filestore/storecompactjob.h b/resources/shared/filestore/storecompactjob.h
new file mode 100644 (file)
index 0000000..0cf7daf
--- /dev/null
@@ -0,0 +1,70 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2009,2010 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef AKONADI_FILESTORE_STORECOMPACTJOB_H
+#define AKONADI_FILESTORE_STORECOMPACTJOB_H
+
+#include "job.h"
+
+#include <Collection>
+#include <Item>
+
+namespace Akonadi
+{
+
+namespace FileStore
+{
+
+/**
+ */
+class AKONADI_FILESTORE_EXPORT StoreCompactJob : public Job
+{
+    friend class AbstractJobSession;
+
+    Q_OBJECT
+
+public:
+    explicit StoreCompactJob(AbstractJobSession *session = Q_NULLPTR);
+
+    virtual ~StoreCompactJob();
+
+    bool accept(Visitor *visitor) Q_DECL_OVERRIDE;
+
+    Item::List changedItems() const;
+
+    Collection::List changedCollections() const;
+
+Q_SIGNALS:
+    void collectionsChanged(const Akonadi::Collection::List &collections);
+    void itemsChanged(const Akonadi::Item::List &items);
+
+private:
+    void handleCollectionsChanged(const Collection::List &collections);
+    void handleItemsChanged(const Item::List &items);
+
+private:
+    class Private;
+    Private *d;
+};
+
+}
+}
+
+#endif
+
diff --git a/resources/shared/filestore/storeinterface.h b/resources/shared/filestore/storeinterface.h
new file mode 100644 (file)
index 0000000..6491324
--- /dev/null
@@ -0,0 +1,88 @@
+/*  This file is part of the KDE project
+    Copyright (C) 2009,2010 Kevin Krammer <kevin.krammer@gmx.at>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to
+    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1301, USA.
+*/
+
+#ifndef AKONADI_FILESTORE_STOREINTERFACE_H
+#define AKONADI_FILESTORE_STOREINTERFACE_H
+
+#include "akonadi-filestore_export.h"
+
+// TODO not nice, collection fetch type should probably be in its own header
+#include "collectionfetchjob.h"
+
+namespace Akonadi
+{
+class Collection;
+class Item;
+
+namespace FileStore
+{
+class CollectionCreateJob;
+class CollectionDeleteJob;
+class CollectionFetchJob;
+class CollectionModifyJob;
+class CollectionMoveJob;
+class ItemCreateJob;
+class ItemDeleteJob;
+class ItemFetchJob;
+class ItemModifyJob;
+class ItemMoveJob;
+class StoreCompactJob;
+
+/**
+ */
+class AKONADI_FILESTORE_EXPORT StoreInterface
+{
+public:
+    virtual ~StoreInterface() {}
+
+    virtual Collection topLevelCollection() const = 0;
+
+    virtual CollectionCreateJob *createCollection(const Collection &collection, const Collection &targetParent) = 0;
+
+    virtual CollectionFetchJob *fetchCollections(const Collection &collection, CollectionFetchJob::Type type = CollectionFetchJob::FirstLevel) const = 0;
+
+    virtual CollectionDeleteJob *deleteCollection(const Collection &collection) = 0;
+
+    virtual CollectionModifyJob *modifyCollection(const Collection &collection) = 0;
+
+    virtual CollectionMoveJob *moveCollection(const Collection &collection, const Collection &targetParent) = 0;
+
+    virtual ItemFetchJob *fetchItems(const Collection &collection) const = 0;
+
+    virtual ItemFetchJob *fetchItem(const Item &item) const = 0;
+
+    virtual ItemCreateJob *createItem(const Item &item, const Collection &collection) = 0;
+
+    virtual ItemModifyJob *modifyItem(const Item &item) = 0;
+
+    virtual ItemDeleteJob *deleteItem(const Item &item) = 0;
+
+    virtual ItemMoveJob *moveItem(const Item &item, const Collection &targetParent) = 0;
+
+    virtual StoreCompactJob *compactStore() = 0;
+
+protected:
+    virtual void setTopLevelCollection(const Collection &collection) = 0;
+};
+
+}
+}
+
+#endif
+
diff --git a/resources/shared/singlefileresource/CMakeLists.txt b/resources/shared/singlefileresource/CMakeLists.txt
new file mode 100644 (file)
index 0000000..6b59a88
--- /dev/null
@@ -0,0 +1,56 @@
+
+project(akonadi-singlefileresource)
+
+
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_singlefile_resource\")
+
+set( AKONADI_SINGLEFILERESOURCE_SHARED_SOURCES
+  singlefileresourcebase.cpp
+  singlefileresourceconfigdialogbase.cpp
+)
+
+set( AKONADI_SINGLEFILERESOURCE_SHARED_UI
+  singlefileresourceconfigdialog_desktop.ui
+  singlefileresourceconfigdialog.ui
+)
+
+set( AKONADI_COLLECTIONATTRIBUTES_SHARED_SOURCES
+  collectionannotationsattribute.cpp
+  collectionflagsattribute.cpp
+)
+
+set( AKONADI_IMAPATTRIBUTES_SHARED_SOURCES
+  imapaclattribute.cpp
+  imapquotaattribute.cpp
+)
+
+
+set(akonadi-singlefileresource_SRCS
+  ${AKONADI_SINGLEFILERESOURCE_SHARED_SOURCES}
+  ${AKONADI_COLLECTIONATTRIBUTES_SHARED_SOURCES}
+  ${AKONADI_IMAPATTRIBUTES_SHARED_SOURCES}
+  createandsettagsjob.cpp
+)
+
+ki18n_wrap_ui(akonadi-singlefileresource_SRCS ${AKONADI_SINGLEFILERESOURCE_SHARED_UI} settingsdialog.ui)
+
+add_library(akonadi-singlefileresource  ${akonadi-singlefileresource_SRCS} )
+generate_export_header(akonadi-singlefileresource BASE_NAME akonadi-singlefileresource)
+
+target_link_libraries(akonadi-singlefileresource
+PUBLIC
+    KF5::I18n
+PRIVATE
+    KF5::AkonadiCore
+    KF5::AkonadiAgentBase
+    KF5::KIOCore
+    KF5::IMAP
+    KF5::KDELibs4Support
+)
+
+set_target_properties(akonadi-singlefileresource PROPERTIES VERSION ${KDEPIMRUNTIME_LIB_VERSION} SOVERSION ${KDEPIMRUNTIME_LIB_SOVERSION} )
+install(TARGETS akonadi-singlefileresource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP)
+
+if (BUILD_TESTING)
+   add_subdirectory(autotests)
+endif()
diff --git a/resources/shared/singlefileresource/Messages.sh b/resources/shared/singlefileresource/Messages.sh
new file mode 100644 (file)
index 0000000..798c5a2
--- /dev/null
@@ -0,0 +1,4 @@
+#! /usr/bin/env bash
+$EXTRACTRC `find . -name \*.ui` >> rc.cpp || exit 11
+$XGETTEXT *.cpp *.h -o $podir/akonadi_singlefile_resource.pot
+rm -f rc.cpp
diff --git a/resources/shared/singlefileresource/autotests/CMakeLists.txt b/resources/shared/singlefileresource/autotests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..6f510a2
--- /dev/null
@@ -0,0 +1,16 @@
+include(ECMAddTests)
+
+find_package(Qt5Test CONFIG REQUIRED)
+
+macro(_add_test _source)
+  set(_test ${_source})
+  get_filename_component(_name ${_source} NAME_WE)
+  add_executable( ${_name} ${_test} )
+  add_test( ${_name} ${_name} )
+  ecm_mark_as_test(knotes-${_name})
+  target_link_libraries(${_name} Qt5::Test   
+                        KF5::AkonadiCore KF5::IMAP)
+endmacro()
+
+_add_test( collectionannotationattributetest.cpp )
+_add_test( imapaclattributetest.cpp )
diff --git a/resources/shared/singlefileresource/autotests/collectionannotationattributetest.cpp b/resources/shared/singlefileresource/autotests/collectionannotationattributetest.cpp
new file mode 100644 (file)
index 0000000..0532b0b
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+    Copyright (C) 2009 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include <QtTest>
+#include <qtest.h>
+#include "../collectionannotationsattribute.cpp"
+
+typedef QMap<QByteArray, QByteArray> Annotation;
+Q_DECLARE_METATYPE(Annotation)
+
+using namespace Akonadi;
+
+class CollectionAnnotationAttributeTest : public QObject
+{
+    Q_OBJECT
+private Q_SLOTS:
+    void testSerializeDeserialize_data()
+    {
+        QTest::addColumn<Annotation>("annotation");
+
+        Annotation a;
+        QTest::newRow("empty") << a;
+
+        a.insert("/vendor/cmu/cyrus-imapd/lastpop", "");
+        QTest::newRow("empty value, single key") << a;
+
+        a.insert("/vendor/cmu/cyrus-imapd/condstore", "false");
+        QTest::newRow("empty value, two keys") << a;
+
+        a.insert("/vendor/cmu/cyrus-imapd/sharedseen", "false");
+        QTest::newRow("empty value, three keys") << a;
+
+        a.clear();
+        a.insert("vendor/cmu/cyrus-imapd/lastpop", " ");
+        QTest::newRow("space value, single key") << a;
+
+        a.insert("/vendor/cmu/cyrus-imapd/condstore", "false");
+        QTest::newRow("space value, two keys") << a;
+
+        a.insert("/vendor/cmu/cyrus-imapd/sharedseen", "false");
+        QTest::newRow("space value, three keys") << a;
+    }
+
+    void testSerializeDeserialize()
+    {
+        QFETCH(Annotation, annotation);
+        CollectionAnnotationsAttribute *attr1 = new CollectionAnnotationsAttribute();
+        attr1->setAnnotations(annotation);
+        QCOMPARE(attr1->annotations(), annotation);
+
+        CollectionAnnotationsAttribute *attr2 = new CollectionAnnotationsAttribute();
+        attr2->deserialize(attr1->serialized());
+        QCOMPARE(attr2->annotations(), annotation);
+
+        CollectionAnnotationsAttribute *attr3 = new CollectionAnnotationsAttribute();
+        attr3->setAnnotations(attr2->annotations());
+        QCOMPARE(attr3->serialized(), attr1->serialized());
+
+        delete attr1;
+        delete attr2;
+        delete attr3;
+    }
+
+};
+
+QTEST_MAIN(CollectionAnnotationAttributeTest)
+
+#include "collectionannotationattributetest.moc"
+
diff --git a/resources/shared/singlefileresource/autotests/imapaclattributetest.cpp b/resources/shared/singlefileresource/autotests/imapaclattributetest.cpp
new file mode 100644 (file)
index 0000000..0544633
--- /dev/null
@@ -0,0 +1,256 @@
+/*
+    Copyright (C) 2010 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include <QtTest>
+#include <qtest.h>
+#include "../imapaclattribute.cpp"
+
+using namespace Akonadi;
+
+typedef QMap<QByteArray, KIMAP::Acl::Rights> ImapAcl;
+
+Q_DECLARE_METATYPE(ImapAcl)
+Q_DECLARE_METATYPE(KIMAP::Acl::Rights)
+
+class ImapAclAttributeTest : public QObject
+{
+    Q_OBJECT
+
+private Q_SLOTS:
+    void shouldHaveDefaultValue()
+    {
+        ImapAclAttribute attr;
+        QVERIFY(attr.oldRights().isEmpty());
+        QVERIFY(attr.rights().isEmpty());
+        QVERIFY(!attr.myRights());
+    }
+
+    void shouldBuildAttribute()
+    {
+        QMap<QByteArray, KIMAP::Acl::Rights> right;
+        right.insert("test", KIMAP::Acl::Admin);
+        right.insert("foo", KIMAP::Acl::Admin);
+
+        QMap<QByteArray, KIMAP::Acl::Rights> oldright;
+        right.insert("test", KIMAP::Acl::Delete);
+        right.insert("foo", KIMAP::Acl::Delete);
+        ImapAclAttribute attr(right, oldright);
+        QCOMPARE(attr.oldRights(), oldright);
+        QCOMPARE(attr.rights(), right);
+    }
+
+    void shouldAssignValue()
+    {
+        ImapAclAttribute attr;
+        QMap<QByteArray, KIMAP::Acl::Rights> right;
+        right.insert("test", KIMAP::Acl::Admin);
+        right.insert("foo", KIMAP::Acl::Admin);
+        attr.setRights(right);
+        QCOMPARE(attr.rights(), right);
+    }
+
+    void shouldCloneAttr()
+    {
+        ImapAclAttribute attr;
+        QMap<QByteArray, KIMAP::Acl::Rights> right;
+        right.insert("test", KIMAP::Acl::Admin);
+        right.insert("foo", KIMAP::Acl::Admin);
+        attr.setRights(right);
+        ImapAclAttribute *clone = attr.clone();
+        QVERIFY(attr == *clone);
+        delete clone;
+    }
+
+    void shouldSerializedAttribute()
+    {
+        QMap<QByteArray, KIMAP::Acl::Rights> right;
+        right.insert("test", KIMAP::Acl::Admin);
+        right.insert("foo", KIMAP::Acl::Admin);
+
+        QMap<QByteArray, KIMAP::Acl::Rights> oldright;
+        right.insert("test", KIMAP::Acl::Delete);
+        right.insert("foo", KIMAP::Acl::Delete);
+        ImapAclAttribute attr(right, oldright);
+        const QByteArray ba = attr.serialized();
+        ImapAclAttribute result;
+        result.deserialize(ba);
+        QVERIFY(attr == result);
+    }
+
+    void shouldHaveType()
+    {
+        ImapAclAttribute attr;
+        QCOMPARE(attr.type(), QByteArray("imapacl"));
+    }
+
+    void testMyRights()
+    {
+        ImapAclAttribute attr;
+        KIMAP::Acl::Rights myRight = KIMAP::Acl::Admin;
+
+        attr.setMyRights(myRight);
+        QCOMPARE(attr.myRights(), myRight);
+
+        ImapAclAttribute *clone = attr.clone();
+        QCOMPARE(clone->myRights(), myRight);
+
+        QVERIFY(*clone == attr);
+
+        clone->setMyRights(KIMAP::Acl::Custom0);
+        QVERIFY(!(*clone == attr));
+        delete clone;
+    }
+
+    void testDeserialize_data()
+    {
+        QTest::addColumn<ImapAcl>("rights");
+        QTest::addColumn<KIMAP::Acl::Rights>("myRights");
+        QTest::addColumn<QByteArray>("serialized");
+
+        KIMAP::Acl::Rights rights = KIMAP::Acl::None;
+
+        {
+            ImapAcl acl;
+            QTest::newRow("empty") << acl << KIMAP::Acl::Rights(KIMAP::Acl::None) << QByteArray(" %% ");
+        }
+
+        {
+            ImapAcl acl;
+            acl.insert("user@host", rights);
+            QTest::newRow("none") << acl << KIMAP::Acl::Rights(KIMAP::Acl::None) << QByteArray("user@host  %% ");
+        }
+
+        {
+            ImapAcl acl;
+            acl.insert("user@host", KIMAP::Acl::Lookup);
+            QTest::newRow("lookup") << acl << KIMAP::Acl::Rights(KIMAP::Acl::None) << QByteArray("user@host l %% ");
+        }
+
+        {
+            ImapAcl acl;
+            acl.insert("user@host", KIMAP::Acl::Lookup | KIMAP::Acl::Read);
+            QTest::newRow("lookup/read") << acl << KIMAP::Acl::Rights(KIMAP::Acl::None) << QByteArray("user@host lr %% ");
+        }
+
+        {
+            ImapAcl acl;
+            acl.insert("user@host", KIMAP::Acl::Lookup | KIMAP::Acl::Read);
+            acl.insert("otheruser@host", KIMAP::Acl::Lookup | KIMAP::Acl::Read);
+            QTest::newRow("lookup/read") << acl << KIMAP::Acl::Rights(KIMAP::Acl::None) << QByteArray("otheruser@host lr % user@host lr %% ");
+        }
+
+        {
+            QTest::newRow("myrights") << ImapAcl() << KIMAP::Acl::rightsFromString("lrswipckxtdaen") << QByteArray(" %%  %% lrswipckxtdaen");
+        }
+    }
+
+    void testDeserialize()
+    {
+        QFETCH(ImapAcl, rights);
+        QFETCH(KIMAP::Acl::Rights, myRights);
+        QFETCH(QByteArray, serialized);
+
+        ImapAclAttribute deserializeAttr;
+        deserializeAttr.deserialize(serialized);
+        QCOMPARE(deserializeAttr.rights(), rights);
+        QCOMPARE(deserializeAttr.myRights(), myRights);
+    }
+
+    void testSerializeDeserialize_data()
+    {
+        QTest::addColumn<ImapAcl>("rights");
+        QTest::addColumn<KIMAP::Acl::Rights>("myRights");
+        QTest::addColumn<QByteArray>("serialized");
+        QTest::addColumn<QByteArray>("oldSerialized");
+
+        ImapAcl acl;
+        QTest::newRow("empty") << acl << KIMAP::Acl::Rights(KIMAP::Acl::None) << QByteArray(" %% ") << QByteArray("testme@host l %% ");
+
+        acl.insert("user@host", KIMAP::Acl::None);
+        QTest::newRow("none") << acl << KIMAP::Acl::Rights(KIMAP::Acl::None) << QByteArray("user@host  %% ") << QByteArray("testme@host l %% user@host ");
+
+        acl.insert("user@host", KIMAP::Acl::Lookup);
+        QTest::newRow("lookup") << acl << KIMAP::Acl::Rights(KIMAP::Acl::None) << QByteArray("user@host l %% ") << QByteArray("testme@host l %% user@host l");
+
+        acl.insert("user@host", KIMAP::Acl::Lookup | KIMAP::Acl::Read);
+        QTest::newRow("lookup/read") << acl << KIMAP::Acl::Rights(KIMAP::Acl::None)  << QByteArray("user@host lr %% ") << QByteArray("testme@host l %% user@host lr");
+
+        acl.insert("otheruser@host", KIMAP::Acl::Lookup | KIMAP::Acl::Read);
+        QTest::newRow("lookup/read") << acl << KIMAP::Acl::Rights(KIMAP::Acl::None) << QByteArray("otheruser@host lr % user@host lr %% ")
+                                     << QByteArray("testme@host l %% otheruser@host lr % user@host lr");
+
+        QTest::newRow("myrights") << acl << KIMAP::Acl::rightsFromString("lrswipckxtdaen") << QByteArray("otheruser@host lr % user@host lr %%  %% lrswipckxtdaen")
+                                  << QByteArray("testme@host l %% otheruser@host lr % user@host lr %% lrswipckxtdaen");
+    }
+
+    void testSerializeDeserialize()
+    {
+        QFETCH(ImapAcl, rights);
+        QFETCH(KIMAP::Acl::Rights, myRights);
+        QFETCH(QByteArray, serialized);
+        QFETCH(QByteArray, oldSerialized);
+
+        ImapAclAttribute *attr = new ImapAclAttribute();
+        attr->setRights(rights);
+        attr->setMyRights(myRights);
+        QCOMPARE(attr->serialized(), serialized);
+
+        ImapAcl acl;
+        acl.insert("testme@host", KIMAP::Acl::Lookup);
+        attr->setRights(acl);
+
+        QCOMPARE(attr->serialized(), oldSerialized);
+
+        delete attr;
+
+        ImapAclAttribute deserializeAttr;
+        deserializeAttr.deserialize(serialized);
+        QCOMPARE(deserializeAttr.rights(), rights);
+        QCOMPARE(deserializeAttr.myRights(), myRights);
+    }
+
+    void testOldRights()
+    {
+        ImapAcl acls;
+        acls.insert("first_user@host", KIMAP::Acl::Lookup | KIMAP::Acl::Read);
+        acls.insert("second_user@host", KIMAP::Acl::Lookup | KIMAP::Acl::Read);
+        acls.insert("third_user@host", KIMAP::Acl::Lookup | KIMAP::Acl::Read);
+
+        ImapAclAttribute *attr = new ImapAclAttribute();
+        attr->setRights(acls);
+
+        ImapAcl oldAcls = acls;
+
+        acls.remove("first_user@host");
+        acls.remove("third_user@host");
+
+        attr->setRights(acls);
+
+        QCOMPARE(attr->oldRights(), oldAcls);
+
+        attr->setRights(acls);
+
+        QCOMPARE(attr->oldRights(), acls);
+        delete attr;
+    }
+};
+
+QTEST_MAIN(ImapAclAttributeTest)
+
+#include "imapaclattributetest.moc"
diff --git a/resources/shared/singlefileresource/collectionannotationsattribute.cpp b/resources/shared/singlefileresource/collectionannotationsattribute.cpp
new file mode 100644 (file)
index 0000000..bc1f97e
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+    Copyright (C) 2008 Omat Holding B.V. <info@omat.nl>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "collectionannotationsattribute.h"
+
+#include <QDebug>
+#include <QByteArray>
+#include <attribute.h>
+
+using namespace Akonadi;
+
+CollectionAnnotationsAttribute::CollectionAnnotationsAttribute()
+{
+}
+
+CollectionAnnotationsAttribute::CollectionAnnotationsAttribute(const QMap<QByteArray, QByteArray> &annotations)
+    : mAnnotations(annotations)
+{
+}
+
+void CollectionAnnotationsAttribute::setAnnotations(const QMap<QByteArray, QByteArray> &annotations)
+{
+    mAnnotations = annotations;
+}
+
+QMap<QByteArray, QByteArray> CollectionAnnotationsAttribute::annotations() const
+{
+    return mAnnotations;
+}
+
+QByteArray CollectionAnnotationsAttribute::type() const
+{
+    static const QByteArray sType("collectionannotations");
+    return sType;
+}
+
+Akonadi::Attribute *CollectionAnnotationsAttribute::clone() const
+{
+    return new CollectionAnnotationsAttribute(mAnnotations);
+}
+
+QByteArray CollectionAnnotationsAttribute::serialized() const
+{
+    QByteArray result = "";
+
+    foreach (const QByteArray &key, mAnnotations.keys()) {
+        result += key;
+        result += ' ';
+        result += mAnnotations[key];
+        result += " % "; // We use this separator as '%' is not allowed in keys or values
+    }
+    result.chop(3);
+
+    return result;
+}
+
+void CollectionAnnotationsAttribute::deserialize(const QByteArray &data)
+{
+    mAnnotations.clear();
+    const QList<QByteArray> lines = data.split('%');
+
+    for (int i = 0; i < lines.size(); ++i) {
+        QByteArray line = lines[i];
+        if (i != 0 && line.startsWith(' ')) {
+            line = line.mid(1);
+        }
+        if (i != lines.size() - 1 && line.endsWith(' ')) {
+            line.chop(1);
+        }
+        if (line.trimmed().isEmpty()) {
+            continue;
+        }
+        int wsIndex = line.indexOf(' ');
+        if (wsIndex > 0) {
+            const QByteArray key = line.mid(0, wsIndex);
+            const QByteArray value = line.mid(wsIndex + 1);
+            mAnnotations[key] = value;
+        } else {
+            mAnnotations.insert(line, QByteArray());
+        }
+    }
+}
diff --git a/resources/shared/singlefileresource/collectionannotationsattribute.h b/resources/shared/singlefileresource/collectionannotationsattribute.h
new file mode 100644 (file)
index 0000000..9d32b86
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+    Copyright (C) 2008 Omat Holding B.V. <info@omat.nl>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef AKONADI_COLLECTIONANNOTATIONSATTRIBUTE_H
+#define AKONADI_COLLECTIONANNOTATIONSATTRIBUTE_H
+
+#include <attribute.h>
+#include "akonadi-singlefileresource_export.h"
+#include <QtCore/QMap>
+
+namespace Akonadi
+{
+
+class AKONADI_SINGLEFILERESOURCE_EXPORT CollectionAnnotationsAttribute : public Akonadi::Attribute
+{
+public:
+    CollectionAnnotationsAttribute();
+    CollectionAnnotationsAttribute(const QMap<QByteArray, QByteArray> &annotations);
+    void setAnnotations(const QMap<QByteArray, QByteArray> &annotations);
+    QMap<QByteArray, QByteArray> annotations() const;
+    QByteArray type() const Q_DECL_OVERRIDE;
+    Attribute *clone() const Q_DECL_OVERRIDE;
+    QByteArray serialized() const Q_DECL_OVERRIDE;
+    void deserialize(const QByteArray &data) Q_DECL_OVERRIDE;
+
+private:
+    QMap<QByteArray, QByteArray> mAnnotations;
+};
+
+}
+
+#endif
diff --git a/resources/shared/singlefileresource/collectionflagsattribute.cpp b/resources/shared/singlefileresource/collectionflagsattribute.cpp
new file mode 100644 (file)
index 0000000..63b75c6
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+    Copyright (C) 2008 Omat Holding B.V. <info@omat.nl>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "collectionflagsattribute.h"
+
+#include <QByteArray>
+#include <attribute.h>
+
+using namespace Akonadi;
+
+CollectionFlagsAttribute::CollectionFlagsAttribute(const QList<QByteArray> &flags)
+    : mFlags(flags)
+{
+}
+
+void CollectionFlagsAttribute::setFlags(const QList<QByteArray> &flags)
+{
+    mFlags = flags;
+}
+
+QList<QByteArray> CollectionFlagsAttribute::flags() const
+{
+    return mFlags;
+}
+
+QByteArray CollectionFlagsAttribute::type() const
+{
+    static const QByteArray sType("collectionflags");
+    return sType;
+}
+
+Akonadi::Attribute *CollectionFlagsAttribute::clone() const
+{
+    return new CollectionFlagsAttribute(mFlags);
+}
+
+QByteArray CollectionFlagsAttribute::serialized() const
+{
+    QByteArray result;
+
+    foreach (const QByteArray &flag, mFlags) {
+        result += flag + ' ';
+    }
+    result.chop(1);
+
+    return result;
+}
+
+void CollectionFlagsAttribute::deserialize(const QByteArray &data)
+{
+    mFlags = data.split(' ');
+}
diff --git a/resources/shared/singlefileresource/collectionflagsattribute.h b/resources/shared/singlefileresource/collectionflagsattribute.h
new file mode 100644 (file)
index 0000000..f967fde
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+    Copyright (C) 2008 Omat Holding B.V. <info@omat.nl>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef AKONADI_COLLECTIONFLAGSATTRIBUTE_H
+#define AKONADI_COLLECTIONFLAGSATTRIBUTE_H
+
+#include <attribute.h>
+#include "akonadi-singlefileresource_export.h"
+
+namespace Akonadi
+{
+
+class AKONADI_SINGLEFILERESOURCE_EXPORT CollectionFlagsAttribute : public Akonadi::Attribute
+{
+public:
+    explicit CollectionFlagsAttribute(const QList<QByteArray> &flags = QList<QByteArray>());
+    void setFlags(const QList<QByteArray> &flags);
+    QList<QByteArray> flags() const;
+    QByteArray type() const Q_DECL_OVERRIDE;
+    Attribute *clone() const Q_DECL_OVERRIDE;
+    QByteArray serialized() const Q_DECL_OVERRIDE;
+    void deserialize(const QByteArray &data) Q_DECL_OVERRIDE;
+
+private:
+    QList<QByteArray> mFlags;
+};
+
+}
+
+#endif
diff --git a/resources/shared/singlefileresource/createandsettagsjob.cpp b/resources/shared/singlefileresource/createandsettagsjob.cpp
new file mode 100644 (file)
index 0000000..8277a7f
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ *  Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+ *
+ *  This library is free software; you can redistribute it and/or modify it
+ *  under the terms of the GNU Library General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or (at your
+ *  option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful, but WITHOUT
+ *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ *  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ *  License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public License
+ *  along with this library; see the file COPYING.LIB.  If not, write to the
+ *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ *  02110-1301, USA.
+ */
+#include "createandsettagsjob.h"
+#include <AkonadiCore/tagcreatejob.h>
+#include <AkonadiCore/itemmodifyjob.h>
+
+#include <QDebug>
+
+using namespace Akonadi;
+
+CreateAndSetTagsJob::CreateAndSetTagsJob(const Item &item, const Akonadi::Tag::List &tags, QObject *parent)
+    : KJob(parent),
+      mItem(item),
+      mTags(tags),
+      mCount(0)
+{
+
+}
+
+void CreateAndSetTagsJob::start()
+{
+    if (mTags.isEmpty()) {
+        emitResult();
+    }
+    Q_FOREACH (const Akonadi::Tag &tag, mTags) {
+        Akonadi::TagCreateJob *createJob = new Akonadi::TagCreateJob(tag, this);
+        createJob->setMergeIfExisting(true);
+        connect(createJob, &Akonadi::TagCreateJob::result, this, &CreateAndSetTagsJob::onCreateDone);
+    }
+}
+
+void CreateAndSetTagsJob::onCreateDone(KJob *job)
+{
+    mCount++;
+    if (job->error()) {
+        qWarning() << "Failed to create tag " << job->errorString();
+    } else {
+        Akonadi::TagCreateJob *createJob = static_cast<Akonadi::TagCreateJob *>(job);
+        mCreatedTags << createJob->tag();
+    }
+    if (mCount == mTags.size()) {
+        Q_FOREACH (const Akonadi::Tag &tag, mCreatedTags) {
+            mItem.setTag(tag);
+        }
+        Akonadi::ItemModifyJob *modJob = new Akonadi::ItemModifyJob(mItem, this);
+        connect(modJob, &Akonadi::ItemModifyJob::result, this, &CreateAndSetTagsJob::onModifyDone);
+    }
+}
+
+void CreateAndSetTagsJob::onModifyDone(KJob *job)
+{
+    if (job->error()) {
+        qWarning() << "Failed to modify item " << job->errorString();
+        setError(KJob::UserDefinedError);
+    }
+    emitResult();
+}
diff --git a/resources/shared/singlefileresource/createandsettagsjob.h b/resources/shared/singlefileresource/createandsettagsjob.h
new file mode 100644 (file)
index 0000000..f8fba23
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ *  Copyright (c) 2014 Christian Mollekopf <mollekopf@kolabsys.com>
+ *
+ *  This library is free software; you can redistribute it and/or modify it
+ *  under the terms of the GNU Library General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or (at your
+ *  option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful, but WITHOUT
+ *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ *  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ *  License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public License
+ *  along with this library; see the file COPYING.LIB.  If not, write to the
+ *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ *  02110-1301, USA.
+ */
+#ifndef CREATEANDSETTAGSJOB_H
+#define CREATEANDSETTAGSJOB_H
+
+#include <KJob>
+#include <item.h>
+#include <tag.h>
+#include "akonadi-singlefileresource_export.h"
+
+class AKONADI_SINGLEFILERESOURCE_EXPORT CreateAndSetTagsJob : public KJob
+{
+    Q_OBJECT
+public:
+    explicit CreateAndSetTagsJob(const Akonadi::Item &item, const Akonadi::Tag::List &tags, QObject *parent = Q_NULLPTR);
+
+    void start() Q_DECL_OVERRIDE;
+
+private Q_SLOTS:
+    void onCreateDone(KJob *);
+    void onModifyDone(KJob *);
+
+private:
+    Akonadi::Item mItem;
+    Akonadi::Tag::List mTags;
+    Akonadi::Tag::List mCreatedTags;
+    int mCount;
+};
+
+#endif
diff --git a/resources/shared/singlefileresource/getcredentialsjob.cpp b/resources/shared/singlefileresource/getcredentialsjob.cpp
new file mode 100644 (file)
index 0000000..9ba9112
--- /dev/null
@@ -0,0 +1,102 @@
+/*************************************************************************************
+ *  Copyright (C) 2013 by Alejandro Fiestas Olivares <afiestas@kde.org>              *
+ *                                                                                   *
+ *  This library is free software; you can redistribute it and/or                    *
+ *  modify it under the terms of the GNU Lesser General Public                       *
+ *  License as published by the Free Software Foundation; either                     *
+ *  version 2 of the License, or (at KDE e.V's discretion) any later version.        *
+ *                                                                                   *
+ *  This library is distributed in the hope that it will be useful,                  *
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of                   *
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU                *
+ *  Library General Public License for more details.                                 *
+ *                                                                                   *
+ *  You should have received a copy of the GNU Library General Public License        *
+ *  along with this library; see the file COPYING.LIB.  If not, write to             *
+ *  the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,             *
+ *  Boston, MA 02110-1301, USA.                                                      *
+ *************************************************************************************/
+
+#include "getcredentialsjob.h"
+
+#include <Accounts/Manager>
+#include <Accounts/Account>
+#include <Accounts/AccountService>
+
+#include <SignOn/Identity>
+
+#include <QDebug>
+#include <KLocalizedString>
+
+GetCredentialsJob::GetCredentialsJob(const Accounts::AccountId &id, QObject *parent)
+    : KJob(parent)
+    , m_id(id)
+    , m_manager(new Accounts::Manager(this))
+{
+
+}
+
+void GetCredentialsJob::start()
+{
+    QMetaObject::invokeMethod(this, "getCredentials", Qt::QueuedConnection);
+}
+
+void GetCredentialsJob::setServiceType(const QString &serviceType)
+{
+    m_serviceType = serviceType;
+}
+
+void GetCredentialsJob::getCredentials()
+{
+    Accounts::Account *acc = m_manager->account(m_id);
+    if (!acc) {
+        setError(-1);
+        setErrorText(i18n("Could not find account"));
+        emitResult();
+        return;
+    }
+    Accounts::AccountService *service = new Accounts::AccountService(acc, m_manager->service(m_serviceType), this);
+
+    Accounts::AuthData authData = service->authData();
+    m_authData = authData.parameters();
+    SignOn::Identity *identity = SignOn::Identity::existingIdentity(authData.credentialsId(), this);
+
+    if (!identity) {
+        setError(-1);
+        setErrorText(i18n("Could not find credentials"));
+        emitResult();
+        return;
+    }
+
+    m_authData[QLatin1String("AccountUsername")] = acc->value(QLatin1String("username")).toString();
+    QPointer<SignOn::AuthSession> authSession = identity->createSession(authData.method());
+
+    connect(authSession, SIGNAL(response(SignOn::SessionData)), SLOT(sessionResponse(SignOn::SessionData)));
+    connect(authSession, SIGNAL(error(SignOn::Error)), SLOT(sessionError(SignOn::Error)));
+
+    authSession->process(authData.parameters(), authData.mechanism());
+}
+
+void GetCredentialsJob::sessionResponse(const SignOn::SessionData &data)
+{
+    m_sessionData = data;
+    emitResult();
+}
+
+void GetCredentialsJob::sessionError(const SignOn::Error &error)
+{
+    qDebug() << error.message();
+    setError(-1);
+    setErrorText(error.message());
+    emitResult();
+}
+
+Accounts::AccountId GetCredentialsJob::accountId() const
+{
+    return m_id;
+}
+
+QVariantMap GetCredentialsJob::credentialsData() const
+{
+    return m_sessionData.toMap().unite(m_authData);
+}
diff --git a/resources/shared/singlefileresource/getcredentialsjob.h b/resources/shared/singlefileresource/getcredentialsjob.h
new file mode 100644 (file)
index 0000000..c09b37a
--- /dev/null
@@ -0,0 +1,64 @@
+/*************************************************************************************
+ *  Copyright (C) 2013 by Alejandro Fiestas Olivares <afiestas@kde.org>              *
+ *                                                                                   *
+ *  This library is free software; you can redistribute it and/or                    *
+ *  modify it under the terms of the GNU Lesser General Public                       *
+ *  License as published by the Free Software Foundation; either                     *
+ *  version 2 of the License, or (at KDE e.V's discretion) any later version.        *
+ *                                                                                   *
+ *  This library is distributed in the hope that it will be useful,                  *
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of                   *
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU                *
+ *  Library General Public License for more details.                                 *
+ *                                                                                   *
+ *  You should have received a copy of the GNU Library General Public License        *
+ *  along with this library; see the file COPYING.LIB.  If not, write to             *
+ *  the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,             *
+ *  Boston, MA 02110-1301, USA.                                                      *
+ *************************************************************************************/
+
+#ifndef GET_CREDENTIALS_JOB_H
+#define GET_CREDENTIALS_JOB_H
+
+#include "akonadi-singlefileresource_export.h"
+#include <kjob.h>
+
+#include <Accounts/Account>
+#include <Accounts/AuthData>
+#include <SignOn/SessionData>
+
+namespace Accounts
+{
+class Manager;
+};
+namespace SignOn
+{
+class Error;
+};
+
+class AKONADI_SINGLEFILERESOURCE_EXPORT GetCredentialsJob : public KJob
+{
+    Q_OBJECT
+public:
+    explicit GetCredentialsJob(const Accounts::AccountId &id, QObject *parent = 0);
+    virtual void start();
+
+    void setServiceType(const QString &serviceType);
+
+    QVariantMap credentialsData() const;
+    Accounts::AccountId accountId() const;
+
+private Q_SLOTS:
+    void getCredentials();
+    void sessionResponse(const SignOn::SessionData &data);
+    void sessionError(const SignOn::Error &error);
+
+private:
+    QString m_serviceType;
+    Accounts::AccountId m_id;
+    QVariantMap m_authData;
+    Accounts::Manager *m_manager;
+    SignOn::SessionData m_sessionData;
+};
+
+#endif //GET_CREDENTIALS_JOB_H
diff --git a/resources/shared/singlefileresource/imapaclattribute.cpp b/resources/shared/singlefileresource/imapaclattribute.cpp
new file mode 100644 (file)
index 0000000..1aac458
--- /dev/null
@@ -0,0 +1,163 @@
+/*
+    Copyright (C) 2009 Kevin Ottens <ervin@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "imapaclattribute.h"
+
+#include <QtCore/QByteArray>
+
+using namespace Akonadi;
+
+ImapAclAttribute::ImapAclAttribute()
+{
+}
+
+ImapAclAttribute::ImapAclAttribute(const QMap<QByteArray, KIMAP::Acl::Rights> &rights,
+                                   const QMap<QByteArray, KIMAP::Acl::Rights> &oldRights)
+    : mRights(rights), mOldRights(oldRights)
+{
+}
+
+void ImapAclAttribute::setRights(const QMap<QByteArray, KIMAP::Acl::Rights> &rights)
+{
+    mOldRights = mRights;
+    mRights = rights;
+}
+
+QMap<QByteArray, KIMAP::Acl::Rights> ImapAclAttribute::rights() const
+{
+    return mRights;
+}
+
+QMap<QByteArray, KIMAP::Acl::Rights> ImapAclAttribute::oldRights() const
+{
+    return mOldRights;
+}
+
+void ImapAclAttribute::setMyRights(KIMAP::Acl::Rights rights)
+{
+    mMyRights = rights;
+}
+
+KIMAP::Acl::Rights ImapAclAttribute::myRights() const
+{
+    return mMyRights;
+}
+
+QByteArray ImapAclAttribute::type() const
+{
+    static const QByteArray sType("imapacl");
+    return sType;
+}
+
+ImapAclAttribute *ImapAclAttribute::clone() const
+{
+    ImapAclAttribute *attr = new ImapAclAttribute(mRights, mOldRights);
+    attr->setMyRights(mMyRights);
+    return attr;
+}
+
+QByteArray ImapAclAttribute::serialized() const
+{
+    QByteArray result = "";
+
+    bool added = false;
+    foreach (const QByteArray &id, mRights.keys()) {
+        result += id;
+        result += ' ';
+        result += KIMAP::Acl::rightsToString(mRights[id]);
+        result += " % "; // We use this separator as '%' is not allowed in keys or values
+        added = true;
+    }
+
+    if (added) {
+        result.chop(3);
+    }
+
+    result += " %% ";
+
+    added = false;
+    foreach (const QByteArray &id, mOldRights.keys()) {
+        result += id;
+        result += ' ';
+        result += KIMAP::Acl::rightsToString(mOldRights[id]);
+        result += " % "; // We use this separator as '%' is not allowed in keys or values
+        added = true;
+    }
+
+    if (added) {
+        result.chop(3);
+    }
+
+    if (mMyRights) {
+        result += " %% ";
+        result += KIMAP::Acl::rightsToString(mMyRights);
+    }
+
+    return result;
+}
+
+static void fillRightsMap(const QList<QByteArray> &rights, QMap <QByteArray, KIMAP::Acl::Rights> &map)
+{
+    foreach (const QByteArray &right, rights) {
+        const QByteArray trimmed = right.trimmed();
+        const int wsIndex = trimmed.indexOf(' ');
+        const QByteArray id = trimmed.mid(0, wsIndex).trimmed();
+        if (!id.isEmpty()) {
+            const bool noValue = (wsIndex == -1);
+            if (noValue) {
+                map[id] = KIMAP::Acl::None;
+            } else {
+                const QByteArray value = trimmed.mid(wsIndex + 1, right.length() - wsIndex).trimmed();
+                map[id] = KIMAP::Acl::rightsFromString(value);
+            }
+        }
+    }
+}
+
+void ImapAclAttribute::deserialize(const QByteArray &data)
+{
+    mRights.clear();
+    mOldRights.clear();
+    mMyRights = KIMAP::Acl::None;
+
+    QList<QByteArray> parts;
+    int lastPos = 0;
+    int pos;
+    while ((pos = data.indexOf(" %% ", lastPos)) != -1) {
+        parts << data.mid(lastPos, pos - lastPos);
+        lastPos = pos + 4;
+    }
+    parts << data.mid(lastPos);
+
+    if (parts.size() < 2) {
+        return;
+    }
+    fillRightsMap(parts.at(0).split('%'), mRights);
+    fillRightsMap(parts.at(1).split('%'), mOldRights);
+    if (parts.size() >= 3) {
+        mMyRights = KIMAP::Acl::rightsFromString(parts.at(2));
+    }
+}
+
+bool ImapAclAttribute::operator==(const ImapAclAttribute &other) const
+{
+    return (oldRights() == other.oldRights())
+           && (rights() == other.rights())
+           && (myRights() == other.myRights());
+}
diff --git a/resources/shared/singlefileresource/imapaclattribute.h b/resources/shared/singlefileresource/imapaclattribute.h
new file mode 100644 (file)
index 0000000..2a49e01
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+    Copyright (C) 2009 Kevin Ottens <ervin@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef AKONADI_IMAPACLATTRIBUTE_H
+#define AKONADI_IMAPACLATTRIBUTE_H
+
+#include "akonadi-singlefileresource_export.h"
+#include <attribute.h>
+
+#include <QtCore/QMap>
+
+#include <kimap/acl.h>
+
+namespace Akonadi
+{
+
+class AKONADI_SINGLEFILERESOURCE_EXPORT ImapAclAttribute : public Akonadi::Attribute
+{
+public:
+    ImapAclAttribute();
+    ImapAclAttribute(const QMap<QByteArray, KIMAP::Acl::Rights> &rights,
+                     const QMap<QByteArray, KIMAP::Acl::Rights> &oldRights);
+    void setRights(const QMap<QByteArray, KIMAP::Acl::Rights> &rights);
+    QMap<QByteArray, KIMAP::Acl::Rights> rights() const;
+    QMap<QByteArray, KIMAP::Acl::Rights> oldRights() const;
+    void setMyRights(KIMAP::Acl::Rights rights);
+    KIMAP::Acl::Rights myRights() const;
+    QByteArray type() const Q_DECL_OVERRIDE;
+    ImapAclAttribute *clone() const Q_DECL_OVERRIDE;
+    QByteArray serialized() const Q_DECL_OVERRIDE;
+    void deserialize(const QByteArray &data) Q_DECL_OVERRIDE;
+
+    bool operator==(const ImapAclAttribute &other) const;
+private:
+    QMap<QByteArray, KIMAP::Acl::Rights> mRights;
+    QMap<QByteArray, KIMAP::Acl::Rights> mOldRights;
+    KIMAP::Acl::Rights mMyRights;
+};
+
+}
+
+#endif
diff --git a/resources/shared/singlefileresource/imapquotaattribute.cpp b/resources/shared/singlefileresource/imapquotaattribute.cpp
new file mode 100644 (file)
index 0000000..7379903
--- /dev/null
@@ -0,0 +1,196 @@
+/*
+    Copyright (C) 2009 Kevin Ottens <ervin@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "imapquotaattribute.h"
+
+#include <QtCore/QByteArray>
+#include <QtCore/QString>
+#include <QtCore/QStringList>
+
+#include <QDebug>
+
+using namespace Akonadi;
+
+ImapQuotaAttribute::ImapQuotaAttribute()
+{
+}
+
+Akonadi::ImapQuotaAttribute::ImapQuotaAttribute(const QList<QByteArray> &roots,
+        const QList< QMap<QByteArray, qint64> > &limits,
+        const QList< QMap<QByteArray, qint64> > &usages)
+    : mRoots(roots), mLimits(limits), mUsages(usages)
+{
+    Q_ASSERT(roots.size() == limits.size());
+    Q_ASSERT(roots.size() == usages.size());
+}
+
+void Akonadi::ImapQuotaAttribute::setQuotas(const QList<QByteArray> &roots,
+        const QList< QMap<QByteArray, qint64> > &limits,
+        const QList< QMap<QByteArray, qint64> > &usages)
+{
+    Q_ASSERT(roots.size() == limits.size());
+    Q_ASSERT(roots.size() == usages.size());
+
+    mRoots = roots;
+    mLimits = limits;
+    mUsages = usages;
+}
+
+QList<QByteArray> Akonadi::ImapQuotaAttribute::roots() const
+{
+    return mRoots;
+}
+
+QList< QMap<QByteArray, qint64> > Akonadi::ImapQuotaAttribute::limits() const
+{
+    return mLimits;
+}
+
+QList< QMap<QByteArray, qint64> > Akonadi::ImapQuotaAttribute::usages() const
+{
+    return mUsages;
+}
+
+QByteArray ImapQuotaAttribute::type() const
+{
+    static const QByteArray sType("imapquota");
+    return sType;
+}
+
+Akonadi::Attribute *ImapQuotaAttribute::clone() const
+{
+    return new ImapQuotaAttribute(mRoots, mLimits, mUsages);
+}
+
+QByteArray ImapQuotaAttribute::serialized() const
+{
+    QByteArray result = "";
+
+    // First the roots list
+    foreach (const QByteArray &root, mRoots) {
+        result += root + " % ";
+    }
+    result.chop(3);
+
+    result += " %%%% "; // Members separator
+
+    // Then the limit maps list
+    for (int i = 0; i < mRoots.size(); ++i) {
+        const QMap<QByteArray, qint64> limits = mLimits[i];
+        for (auto it = limits.cbegin(), end = limits.cend(); it != end; ++it) {
+            result += it.key();
+            result += " % "; // We use this separator as '%' is not allowed in keys or values
+            result += QByteArray::number(it.value());
+            result += " %% "; // Pairs separator
+        }
+        result.chop(4);
+        result += " %%% "; // Maps separator
+    }
+    result.chop(5);
+
+    result += " %%%% "; // Members separator
+
+    // Then the usage maps list
+    for (int i = 0; i < mRoots.size(); ++i) {
+        const QMap<QByteArray, qint64> usages = mUsages[i];
+        for (auto it = usages.cbegin(), end = usages.cend(); it != end; ++it) {
+            result += it.key();
+            result += " % "; // We use this separator as '%' is not allowed in keys or values
+            result += QByteArray::number(it.value());
+            result += " %% "; // Pairs separator
+        }
+        result.chop(4);
+        result += " %%% "; // Maps separator
+    }
+    result.chop(5);
+
+    return result;
+}
+
+void ImapQuotaAttribute::deserialize(const QByteArray &data)
+{
+    mRoots.clear();
+    mLimits.clear();
+    mUsages.clear();
+
+    // Nothing was saved.
+    if (data.trimmed().isEmpty()) {
+        return;
+    }
+
+    QString string = QString::fromUtf8(data); // QByteArray has no proper split, so we're forced to convert to QString...
+
+    QStringList members = string.split(QStringLiteral("%%%%"));
+
+    // We expect exactly three members (roots, limits and usages), otherwise something is funky
+    if (members.size() != 3) {
+        qWarning() << "We didn't find exactly three members in this quota serialization";
+        return;
+    }
+
+    QStringList roots = members[0].trimmed().split(QStringLiteral(" % "));
+    foreach (const QString &root, roots) {
+        mRoots << root.trimmed().toUtf8();
+    }
+
+    QStringList allLimits = members[1].trimmed().split(QStringLiteral("%%%"));
+
+    foreach (const QString &limits, allLimits) {
+        QMap<QByteArray, qint64> limitsMap;
+        QStringList strLines = limits.split(QStringLiteral("%%"));
+        QList<QByteArray> lines;
+        lines.reserve(strLines.count());
+        foreach (const QString &strLine, strLines) {
+            lines << strLine.trimmed().toUtf8();
+        }
+
+        foreach (const QByteArray &line, lines) {
+            QByteArray trimmed = line.trimmed();
+            int wsIndex = trimmed.indexOf('%');
+            const QByteArray key = trimmed.mid(0, wsIndex).trimmed();
+            const QByteArray value = trimmed.mid(wsIndex + 1, line.length() - wsIndex).trimmed();
+            limitsMap[key] = value.toLongLong();
+        }
+
+        mLimits << limitsMap;
+    }
+
+    QStringList allUsages = members[2].trimmed().split(QStringLiteral("%%%"));
+    mUsages.reserve(allUsages.count());
+
+    foreach (const QString &usages, allUsages) {
+        QMap<QByteArray, qint64> usagesMap;
+        QStringList strLines = usages.split(QStringLiteral("%%"));
+        QList<QByteArray> lines;
+        lines.reserve(strLines.count());
+        foreach (const QString &strLine, strLines) {
+            lines << strLine.trimmed().toUtf8();
+        }
+
+        foreach (const QByteArray &line, lines) {
+            QByteArray trimmed = line.trimmed();
+            int wsIndex = trimmed.indexOf('%');
+            const QByteArray key = trimmed.mid(0, wsIndex).trimmed();
+            const QByteArray value = trimmed.mid(wsIndex + 1, line.length() - wsIndex).trimmed();
+            usagesMap[key] = value.toLongLong();
+        }
+
+        mUsages << usagesMap;
+    }
+}
diff --git a/resources/shared/singlefileresource/imapquotaattribute.h b/resources/shared/singlefileresource/imapquotaattribute.h
new file mode 100644 (file)
index 0000000..5e47742
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+    Copyright (C) 2009 Kevin Ottens <ervin@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef AKONADI_IMAPQUOTAATTRIBUTE_H
+#define AKONADI_IMAPQUOTAATTRIBUTE_H
+
+#include "akonadi-singlefileresource_export.h"
+#include <attribute.h>
+
+#include <QtCore/QMap>
+
+namespace Akonadi
+{
+
+class AKONADI_SINGLEFILERESOURCE_EXPORT ImapQuotaAttribute : public Akonadi::Attribute
+{
+public:
+    ImapQuotaAttribute();
+    ImapQuotaAttribute(const QList<QByteArray> &roots,
+                       const QList< QMap<QByteArray, qint64> > &limits,
+                       const QList< QMap<QByteArray, qint64> > &usages);
+
+    void setQuotas(const QList<QByteArray> &roots,
+                   const QList< QMap<QByteArray, qint64> > &limits,
+                   const QList< QMap<QByteArray, qint64> > &usages);
+
+    QList<QByteArray> roots() const;
+    QList< QMap<QByteArray, qint64> > limits() const;
+    QList< QMap<QByteArray, qint64> > usages() const;
+
+    QByteArray type() const Q_DECL_OVERRIDE;
+    Attribute *clone() const Q_DECL_OVERRIDE;
+    QByteArray serialized() const Q_DECL_OVERRIDE;
+    void deserialize(const QByteArray &data) Q_DECL_OVERRIDE;
+
+private:
+    QList<QByteArray> mRoots;
+    QList< QMap<QByteArray, qint64> > mLimits;
+    QList< QMap<QByteArray, qint64> > mUsages;
+};
+
+}
+
+#endif
diff --git a/resources/shared/singlefileresource/settingsdialog.ui b/resources/shared/singlefileresource/settingsdialog.ui
new file mode 100644 (file)
index 0000000..72e42b1
--- /dev/null
@@ -0,0 +1,229 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SettingsDialog</class>
+ <widget class="QWidget" name="SettingsDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>547</width>
+    <height>386</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QTabWidget" name="tabWidget">
+     <property name="currentIndex">
+      <number>0</number>
+     </property>
+     <widget class="QWidget" name="tab">
+      <attribute name="title">
+       <string>Directory</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_4">
+       <item>
+        <widget class="QGroupBox" name="groupBox_2">
+         <property name="title">
+          <string>Directory Name</string>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_3">
+          <item>
+           <layout class="QHBoxLayout" name="horizontalLayout">
+            <item>
+             <widget class="QLabel" name="label">
+              <property name="text">
+               <string>Director&amp;y:</string>
+              </property>
+              <property name="buddy">
+               <cstring>kcfg_Path</cstring>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="KUrlRequester" name="kcfg_Path"/>
+            </item>
+           </layout>
+          </item>
+          <item>
+           <widget class="QLabel" name="label_3">
+            <property name="text">
+             <string>Select the directory whose contents should be represented by this resource. If the directory does not exist, it will be created.</string>
+            </property>
+            <property name="wordWrap">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <widget class="QGroupBox" name="groupBox">
+         <property name="title">
+          <string>Access Rights</string>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_2">
+          <item>
+           <widget class="QCheckBox" name="kcfg_ReadOnly">
+            <property name="text">
+             <string>Read only</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QLabel" name="label_2">
+            <property name="text">
+             <string>If read-only mode is enabled, no changes will be written to the directory selected above. Read-only mode will be automatically enabled if you do not have write access to the directory.</string>
+            </property>
+            <property name="wordWrap">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <spacer name="verticalSpacer">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>4</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QWidget" name="tab_2">
+      <attribute name="title">
+       <string>Tuning</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_5">
+       <item>
+        <widget class="QLabel" name="label_4">
+         <property name="text">
+          <string>The options on this page allow you to change parameters that balance data safety and consistency against performance. In general you should be careful with changing anything here, the defaults are good enough in most cases.</string>
+         </property>
+         <property name="wordWrap">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout_2">
+         <item>
+          <widget class="QLabel" name="autosaveLabel">
+           <property name="text">
+            <string>Autosave delay:</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="KPluralHandlingSpinBox" name="kcfg_AutosaveInterval">
+           <property name="minimum">
+            <number>0</number>
+           </property>
+           <property name="value">
+            <number>1</number>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <spacer name="horizontalSpacer">
+           <property name="orientation">
+            <enum>Qt::Horizontal</enum>
+           </property>
+           <property name="sizeHint" stdset="0">
+            <size>
+             <width>40</width>
+             <height>20</height>
+            </size>
+           </property>
+          </spacer>
+         </item>
+        </layout>
+       </item>
+       <item>
+        <spacer name="verticalSpacer_2">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>20</width>
+           <height>138</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KUrlRequester</class>
+   <extends>QFrame</extends>
+   <header>kurlrequester.h</header>
+   <container>1</container>
+  </customwidget>
+  <customwidget>
+   <class>KPluralHandlingSpinBox</class>
+   <extends>QSpinBox</extends>
+   <header>KPluralHandlingSpinBox</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>kcfg_ReadOnly</sender>
+   <signal>toggled(bool)</signal>
+   <receiver>kcfg_AutosaveInterval</receiver>
+   <slot>setDisabled(bool)</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>273</x>
+     <y>205</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>101</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>kcfg_ReadOnly</sender>
+   <signal>toggled(bool)</signal>
+   <receiver>autosaveLabel</receiver>
+   <slot>setDisabled(bool)</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>273</x>
+     <y>205</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>56</x>
+     <y>101</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
diff --git a/resources/shared/singlefileresource/singlefileresource.h b/resources/shared/singlefileresource/singlefileresource.h
new file mode 100644 (file)
index 0000000..c2ffe7d
--- /dev/null
@@ -0,0 +1,386 @@
+/*
+    Copyright (c) 2008 Bertjan Broeksema <broeksema@kde.org>
+    Copyright (c) 2008 Volker Krause <vkrause@kde.org>
+    Copyright (c) 2010 David Jarvie <djarvie@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef AKONADI_SINGLEFILERESOURCE_H
+#define AKONADI_SINGLEFILERESOURCE_H
+
+#include "akonadi-singlefileresource_export.h"
+#include "singlefileresourcebase.h"
+#include "singlefileresourceconfigdialog.h"
+
+#include <entitydisplayattribute.h>
+
+#include <kio/job.h>
+#include <KDirWatch>
+#include <KLocalizedString>
+#include <KGlobal>
+
+#include <QFile>
+#include <QDir>
+#include <QPointer>
+#include <QDebug>
+
+namespace Akonadi
+{
+
+/**
+ * Base class for single file based resources.
+ */
+template <typename Settings>
+class AKONADI_SINGLEFILERESOURCE_EXPORT SingleFileResource : public SingleFileResourceBase
+{
+public:
+    SingleFileResource(const QString &id)
+        : SingleFileResourceBase(id)
+        , mSettings(new Settings(config()))
+    {
+        // The resource needs network when the path refers to a non local file.
+        setNeedsNetwork(!QUrl(mSettings->path()).isLocalFile());
+    }
+    ~SingleFileResource()
+    {
+        delete mSettings;
+    }
+
+    /**
+     * Read changes from the backend file.
+     */
+    void readFile(bool taskContext = false) Q_DECL_OVERRIDE {
+        if (KDirWatch::self()->contains(mCurrentUrl.toLocalFile()))
+        {
+            KDirWatch::self()->removeFile(mCurrentUrl.toLocalFile());
+        }
+
+        if (mSettings->path().isEmpty())
+        {
+            const QString message = i18n("No file selected.");
+            qWarning() << message;
+            emit status(NotConfigured, i18n("The resource not configured yet"));
+            if (taskContext) {
+                cancelTask();
+            }
+            return;
+        }
+
+        mCurrentUrl = QUrl(mSettings->path()); // path already has scheme
+        if (mCurrentHash.isEmpty())
+        {
+            // First call to readFile() lets see if there is a hash stored in a
+            // cache file. If both are the same than there is no need to load the
+            // file and synchronize the resource.
+            mCurrentHash = loadHash();
+        }
+
+        if (mCurrentUrl.isLocalFile())
+        {
+            if (mSettings->displayName().isEmpty()
+                    && (name().isEmpty() || name() == identifier()) && !mCurrentUrl.isEmpty()) {
+                setName(mCurrentUrl.fileName());
+            }
+
+            // check if the file does not exist yet, if so, create it
+            if (!QFile::exists(mCurrentUrl.toLocalFile())) {
+                QFile f(mCurrentUrl.toLocalFile());
+
+                // first create try to create the directory the file should be located in
+                QDir dir = QFileInfo(f).dir();
+                if (! dir.exists()) {
+                    dir.mkpath(dir.path());
+                }
+
+                if (f.open(QIODevice::WriteOnly) && f.resize(0)) {
+                    emit status(Idle, i18nc("@info:status", "Ready"));
+                } else {
+                    const QString message = i18n("Could not create file '%1'.", mCurrentUrl.toDisplayString());
+                    qWarning() << message;
+                    emit status(Broken, message);
+                    mCurrentUrl.clear();
+                    if (taskContext) {
+                        cancelTask();
+                    }
+                    return;
+                }
+            }
+
+            // Cache, because readLocalFile will clear mCurrentUrl on failure.
+            const QString localFileName = mCurrentUrl.toLocalFile();
+            if (!readLocalFile(mCurrentUrl.toLocalFile())) {
+                const QString message = i18n("Could not read file '%1'", localFileName);
+                qWarning() << message;
+                emit status(Broken, message);
+                if (taskContext) {
+                    cancelTask();
+                }
+                return;
+            }
+
+            if (mSettings->monitorFile()) {
+                KDirWatch::self()->addFile(mCurrentUrl.toLocalFile());
+            }
+
+            emit status(Idle, i18nc("@info:status", "Ready"));
+        } else { // !mCurrentUrl.isLocalFile()
+            if (mDownloadJob)
+            {
+                const QString message = i18n("Another download is still in progress.");
+                qWarning() << message;
+                emit error(message);
+                if (taskContext) {
+                    cancelTask();
+                }
+                return;
+            }
+
+            if (mUploadJob)
+            {
+                const QString message = i18n("Another file upload is still in progress.");
+                qWarning() << message;
+                emit error(message);
+                if (taskContext) {
+                    cancelTask();
+                }
+                return;
+            }
+
+            KGlobal::ref();
+
+            // NOTE: Test what happens with remotefile -> save, close before save is finished.
+            mDownloadJob = KIO::file_copy(mCurrentUrl, QUrl::fromLocalFile(cacheFile()), -1, KIO::Overwrite | KIO::DefaultFlags | KIO::HideProgressInfo);
+            connect(mDownloadJob, SIGNAL(result(KJob *)),
+                    SLOT(slotDownloadJobResult(KJob *)));
+            connect(mDownloadJob, SIGNAL(percent(KJob *, ulong)),
+                    SLOT(handleProgress(KJob *, ulong)));
+
+            emit status(Running, i18n("Downloading remote file."));
+        }
+
+        const QString display =  mSettings->displayName();
+        if (!display.isEmpty())
+        {
+            setName(display);
+        }
+    }
+
+    void writeFile(const QVariant &task_context) Q_DECL_OVERRIDE {
+        writeFile(task_context.canConvert<bool>()  &&task_context.toBool());
+    }
+
+    /**
+     * Write changes to the backend file.
+     */
+    void writeFile(bool taskContext = false) Q_DECL_OVERRIDE {
+        if (mSettings->readOnly())
+        {
+            const QString message = i18n("Trying to write to a read-only file: '%1'.", mSettings->path());
+            qWarning() << message;
+            emit error(message);
+            if (taskContext) {
+                cancelTask();
+            }
+            return;
+        }
+
+        // We don't use the Settings::self()->path() here as that might have changed
+        // and in that case it would probably cause data lose.
+        if (mCurrentUrl.isEmpty())
+        {
+            const QString message = i18n("No file specified.");
+            qWarning() << message;
+            emit status(Broken, message);
+            if (taskContext) {
+                cancelTask();
+            }
+            return;
+        }
+
+        if (mCurrentUrl.isLocalFile())
+        {
+            KDirWatch::self()->stopScan();
+            const bool writeResult = writeToFile(mCurrentUrl.toLocalFile());
+            // Update the hash so we can detect at fileChanged() if the file actually
+            // did change.
+            mCurrentHash = calculateHash(mCurrentUrl.toLocalFile());
+            saveHash(mCurrentHash);
+            KDirWatch::self()->startScan();
+            if (!writeResult) {
+                qWarning() << "Error writing to file...";
+                if (taskContext) {
+                    cancelTask();
+                }
+                return;
+            }
+            emit status(Idle, i18nc("@info:status", "Ready"));
+
+        } else {
+            // Check if there is a download or an upload in progress.
+            if (mDownloadJob)
+            {
+                const QString message = i18n("A download is still in progress.");
+                qWarning() << message;
+                emit error(message);
+                if (taskContext) {
+                    cancelTask();
+                }
+                return;
+            }
+
+            if (mUploadJob)
+            {
+                const QString message = i18n("Another file upload is still in progress.");
+                qWarning() << message;
+                emit error(message);
+                if (taskContext) {
+                    cancelTask();
+                }
+                return;
+            }
+
+            // Write te items to the locally cached file.
+            if (!writeToFile(cacheFile()))
+            {
+                qWarning() << "Error writing to file";
+                if (taskContext) {
+                    cancelTask();
+                }
+                return;
+            }
+
+            // Update the hash so we can detect at fileChanged() if the file actually
+            // did change.
+            mCurrentHash = calculateHash(cacheFile());
+            saveHash(mCurrentHash);
+
+            KGlobal::ref();
+            // Start a job to upload the locally cached file to the remote location.
+            mUploadJob = KIO::file_copy(QUrl::fromLocalFile(cacheFile()), mCurrentUrl, -1, KIO::Overwrite | KIO::DefaultFlags | KIO::HideProgressInfo);
+            connect(mUploadJob, SIGNAL(result(KJob *)),
+                    SLOT(slotUploadJobResult(KJob *)));
+            connect(mUploadJob, SIGNAL(percent(KJob *, ulong)),
+                    SLOT(handleProgress(KJob *, ulong)));
+
+            emit status(Running, i18n("Uploading cached file to remote location."));
+        }
+        if (taskContext)
+        {
+            taskDone();
+        }
+    }
+
+    void collectionChanged(const Collection &collection) Q_DECL_OVERRIDE {
+        QString newName;
+        if (collection.hasAttribute<EntityDisplayAttribute>())
+        {
+            EntityDisplayAttribute *attr = collection.attribute<EntityDisplayAttribute>();
+            newName = attr->displayName();
+        }
+        const QString oldName = mSettings->displayName();
+        if (newName != oldName)
+        {
+            mSettings->setDisplayName(newName);
+            mSettings->save();
+        }
+        SingleFileResourceBase::collectionChanged(collection);
+    }
+
+    Collection rootCollection() const Q_DECL_OVERRIDE
+    {
+        Collection c;
+        c.setParentCollection(Collection::root());
+        c.setRemoteId(mSettings->path());
+        const QString display = mSettings->displayName();
+        c.setName(display.isEmpty() ? identifier() : display);
+        QStringList mimeTypes;
+        c.setContentMimeTypes(mSupportedMimetypes);
+        if (readOnly()) {
+            c.setRights(Collection::CanChangeCollection);
+        } else {
+            Collection::Rights rights;
+            rights |= Collection::CanChangeItem;
+            rights |= Collection::CanCreateItem;
+            rights |= Collection::CanDeleteItem;
+            rights |= Collection::CanChangeCollection;
+            c.setRights(rights);
+        }
+        EntityDisplayAttribute *attr = c.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
+        attr->setDisplayName(name());
+        attr->setIconName(mCollectionIcon);
+        return c;
+    }
+
+public Q_SLOTS:
+    /**
+     * Display the configuration dialog for the resource.
+     */
+    void configure(WId windowId) Q_DECL_OVERRIDE {
+        QPointer<SingleFileResourceConfigDialog<Settings> > dlg
+        = new SingleFileResourceConfigDialog<Settings>(windowId, mSettings);
+        customizeConfigDialog(dlg);
+        if (dlg->exec() == QDialog::Accepted)
+        {
+            if (dlg) {     // in case is got destroyed
+                configDialogAcceptedActions(dlg);
+            }
+            reloadFile();
+            synchronizeCollectionTree();
+            emit configurationDialogAccepted();
+        } else {
+            emit configurationDialogRejected();
+        }
+        delete dlg;
+    }
+
+protected:
+    /**
+     * Implement in derived classes to customize the configuration dialog
+     * before it is displayed.
+     */
+    virtual void customizeConfigDialog(SingleFileResourceConfigDialog<Settings> *dlg)
+    {
+        Q_UNUSED(dlg);
+    }
+
+    /**
+     * Implement in derived classes to do things when the configuration dialog
+     * has been accepted, before reloadFile() is called.
+     */
+    virtual void configDialogAcceptedActions(SingleFileResourceConfigDialog<Settings> *dlg)
+    {
+        Q_UNUSED(dlg);
+    }
+
+    void retrieveCollections() Q_DECL_OVERRIDE {
+        Collection::List list;
+        list << rootCollection();
+        collectionsRetrieved(list);
+    }
+
+    bool readOnly() const Q_DECL_OVERRIDE
+    {
+        return mSettings->readOnly();
+    }
+
+protected:
+    Settings *mSettings;
+};
+
+}
+
+#endif
diff --git a/resources/shared/singlefileresource/singlefileresourcebase.cpp b/resources/shared/singlefileresource/singlefileresourcebase.cpp
new file mode 100644 (file)
index 0000000..d7f98be
--- /dev/null
@@ -0,0 +1,289 @@
+/*
+    Copyright (c) 2008 Bertjan Broeksema <broeksema@kde.org>
+    Copyright (c) 2008 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "singlefileresourcebase.h"
+
+#include <changerecorder.h>
+#include <entitydisplayattribute.h>
+#include <itemfetchscope.h>
+
+#include <kio/job.h>
+#include <kio/jobuidelegate.h>
+#include <QDebug>
+#include <KDirWatch>
+#include <KLocalizedString>
+
+#include <KGlobal>
+#include <KConfigGroup>
+
+#include <QtCore/QDir>
+#include <QtCore/QCryptographicHash>
+#include <QStandardPaths>
+
+using namespace Akonadi;
+
+SingleFileResourceBase::SingleFileResourceBase(const QString &id)
+    : ResourceBase(id), mDownloadJob(Q_NULLPTR), mUploadJob(Q_NULLPTR)
+{
+    connect(this, &SingleFileResourceBase::reloadConfiguration, this, &SingleFileResourceBase::reloadFile);
+    QTimer::singleShot(0, this, SLOT(readFile()));
+
+    changeRecorder()->itemFetchScope().fetchFullPayload();
+    changeRecorder()->fetchCollection(true);
+
+    connect(changeRecorder(), &ChangeRecorder::changesAdded, this, &SingleFileResourceBase::scheduleWrite);
+
+    connect(KDirWatch::self(), &KDirWatch::dirty, this, &SingleFileResourceBase::fileChanged);
+    connect(KDirWatch::self(), &KDirWatch::created, this, &SingleFileResourceBase::fileChanged);
+
+    //QT5 KLocalizedString::global()->insertCatalog( QLatin1String("akonadi_singlefile_resource") );
+}
+
+KSharedConfig::Ptr SingleFileResourceBase::runtimeConfig() const
+{
+    return KSharedConfig::openConfig(name() + QLatin1String("rc"), KConfig::SimpleConfig, QStandardPaths::CacheLocation);
+}
+
+bool SingleFileResourceBase::readLocalFile(const QString &fileName)
+{
+    const QByteArray newHash = calculateHash(fileName);
+    if (mCurrentHash != newHash) {
+        if (!mCurrentHash.isEmpty()) {
+            // There was a hash stored in the config file or a chached one from
+            // a previous read and it is different from the hash we just read.
+            handleHashChange();
+        }
+
+        if (!readFromFile(fileName)) {
+            mCurrentHash.clear();
+            mCurrentUrl = QUrl(); // reset so we don't accidentally overwrite the file
+            return false;
+        }
+
+        if (mCurrentHash.isEmpty()) {
+            // This is the very first time we read the file so make sure to store
+            // the hash as writeFile() might not be called at all (e.g in case of
+            // read only resources).
+            saveHash(newHash);
+        }
+
+        // Only synchronize when the contents of the file have changed wrt to
+        // the last time this file was read. Before we synchronize first
+        // clearCache is called to make sure that the cached items get the
+        // actual values as present in the file.
+        invalidateCache(rootCollection());
+        synchronize();
+    } else {
+        // The hash didn't change, notify implementing resources about the
+        // actual file name that should be used when reading the file is
+        // necessary.
+        setLocalFileName(fileName);
+    }
+
+    mCurrentHash = newHash;
+    return true;
+}
+
+void SingleFileResourceBase::setLocalFileName(const QString &fileName)
+{
+    // Default implementation.
+    if (!readFromFile(fileName)) {
+        mCurrentHash.clear();
+        mCurrentUrl = QUrl(); // reset so we don't accidentally overwrite the file
+        return;
+    }
+}
+
+QString SingleFileResourceBase::cacheFile() const
+{
+    return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/") + identifier();
+}
+
+QByteArray SingleFileResourceBase::calculateHash(const QString &fileName) const
+{
+    QFile file(fileName);
+    if (!file.exists()) {
+        return QByteArray();
+    }
+
+    if (!file.open(QIODevice::ReadOnly)) {
+        return QByteArray();
+    }
+
+    QCryptographicHash hash(QCryptographicHash::Md5);
+    qint64 blockSize = 512 * 1024; // Read blocks of 512K
+
+    while (!file.atEnd()) {
+        hash.addData(file.read(blockSize));
+    }
+
+    file.close();
+
+    return hash.result();
+}
+
+void SingleFileResourceBase::handleHashChange()
+{
+    // Default implementation does nothing.
+    qDebug() << "The hash has changed.";
+}
+
+QByteArray SingleFileResourceBase::loadHash() const
+{
+    KConfigGroup generalGroup(runtimeConfig(), "General");
+    return QByteArray::fromHex(generalGroup.readEntry<QByteArray>("hash", QByteArray()));
+}
+
+void SingleFileResourceBase::saveHash(const QByteArray &hash) const
+{
+    KSharedConfig::Ptr config = runtimeConfig();
+    KConfigGroup generalGroup(config, "General");
+    generalGroup.writeEntry("hash", hash.toHex());
+    config->sync();
+}
+
+void SingleFileResourceBase::setSupportedMimetypes(const QStringList &mimeTypes, const QString &icon)
+{
+    mSupportedMimetypes = mimeTypes;
+    mCollectionIcon = icon;
+}
+
+void SingleFileResourceBase::collectionChanged(const Akonadi::Collection &collection)
+{
+    const QString newName = collection.displayName();
+    if (collection.hasAttribute<EntityDisplayAttribute>()) {
+        EntityDisplayAttribute *attr = collection.attribute<EntityDisplayAttribute>();
+        if (!attr->iconName().isEmpty()) {
+            mCollectionIcon = attr->iconName();
+        }
+    }
+
+    if (newName != name()) {
+        setName(newName);
+    }
+
+    changeCommitted(collection);
+}
+
+void SingleFileResourceBase::reloadFile()
+{
+    // Update the network setting.
+    setNeedsNetwork(!mCurrentUrl.isEmpty() && !mCurrentUrl.isLocalFile());
+
+    // if we have something loaded already, make sure we write that back in case
+    // the settings changed
+    if (!mCurrentUrl.isEmpty() && !readOnly()) {
+        writeFile();
+    }
+
+    readFile();
+
+    // name or rights could have changed
+    synchronizeCollectionTree();
+}
+
+void SingleFileResourceBase::handleProgress(KJob *, unsigned long pct)
+{
+    Q_EMIT percent(pct);
+}
+
+void SingleFileResourceBase::fileChanged(const QString &fileName)
+{
+    if (fileName != mCurrentUrl.toLocalFile()) {
+        return;
+    }
+
+    const QByteArray newHash = calculateHash(fileName);
+
+    // There is only a need to synchronize when the file was changed by another
+    // process. At this point we're sure that it is the file that the resource
+    // was configured for because of the check at the beginning of this function.
+    if (newHash == mCurrentHash) {
+        return;
+    }
+
+    if (!mCurrentUrl.isEmpty()) {
+        QString lostFoundFileName;
+        const QUrl prevUrl = mCurrentUrl;
+        int i = 0;
+        do {
+            lostFoundFileName = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + identifier() + QDir::separator() + prevUrl.fileName() + QLatin1Char('-') + QString::number(++i);
+        } while (QFile(lostFoundFileName).exists());
+
+        // create the directory if it doesn't exist yet
+        QDir dir = QFileInfo(lostFoundFileName).dir();
+        if (!dir.exists()) {
+            dir.mkpath(dir.path());
+        }
+
+        mCurrentUrl = QUrl::fromLocalFile(lostFoundFileName);
+        writeFile();
+        mCurrentUrl = prevUrl;
+
+        const QString message = i18n("The file '%1' was changed on disk. "
+                                     "As a precaution, a backup of its previous contents has been created at '%2'.",
+                                     prevUrl.toDisplayString(), QUrl::fromLocalFile(lostFoundFileName).toDisplayString());
+        Q_EMIT warning(message);
+    }
+
+    readFile();
+
+    // Notify resources, so that information bound to the file like indexes etc.
+    // can be updated.
+    handleHashChange();
+    invalidateCache(rootCollection());
+    synchronize();
+}
+
+void SingleFileResourceBase::scheduleWrite()
+{
+    scheduleCustomTask(this, "writeFile", QVariant(true), ResourceBase::AfterChangeReplay);
+}
+
+void SingleFileResourceBase::slotDownloadJobResult(KJob *job)
+{
+    if (job->error() && job->error() != KIO::ERR_DOES_NOT_EXIST) {
+        const QString message = i18n("Could not load file '%1'.", mCurrentUrl.toDisplayString());
+        qWarning() << message;
+        Q_EMIT status(Broken, message);
+    } else {
+        readLocalFile(QUrl::fromLocalFile(cacheFile()).toLocalFile());
+    }
+
+    mDownloadJob = Q_NULLPTR;
+    KGlobal::deref();
+
+    Q_EMIT status(Idle, i18nc("@info:status", "Ready"));
+}
+
+void SingleFileResourceBase::slotUploadJobResult(KJob *job)
+{
+    if (job->error()) {
+        const QString message = i18n("Could not save file '%1'.", mCurrentUrl.toDisplayString());
+        qWarning() << message;
+        Q_EMIT status(Broken, message);
+    }
+
+    mUploadJob = Q_NULLPTR;
+    KGlobal::deref();
+
+    Q_EMIT status(Idle, i18nc("@info:status", "Ready"));
+}
+
diff --git a/resources/shared/singlefileresource/singlefileresourcebase.h b/resources/shared/singlefileresource/singlefileresourcebase.h
new file mode 100644 (file)
index 0000000..81b3825
--- /dev/null
@@ -0,0 +1,189 @@
+/*
+    Copyright (c) 2008 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef AKONADI_SINGLEFILERESOURCEBASE_H
+#define AKONADI_SINGLEFILERESOURCEBASE_H
+
+#include "akonadi-singlefileresource_export.h"
+#include <resourcebase.h>
+
+#include <QUrl>
+#include <QtCore/QStringList>
+#include <QtCore/QTimer>
+
+namespace KIO
+{
+class FileCopyJob;
+class Job;
+}
+
+namespace Akonadi
+{
+
+/**
+ * Base class for single file based resources.
+ * @see SingleFileResource
+ */
+class AKONADI_SINGLEFILERESOURCE_EXPORT SingleFileResourceBase : public ResourceBase, public AgentBase::Observer
+{
+    Q_OBJECT
+public:
+    explicit SingleFileResourceBase(const QString &id);
+
+    /**
+     * Set the mimetypes supported by this resource and an optional icon for the collection.
+     */
+    void setSupportedMimetypes(const QStringList &mimeTypes, const QString &icon = QString());
+
+    void collectionChanged(const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+
+public Q_SLOTS:
+    void reloadFile();
+
+    /*
+     * Read the current state from a file. This can happen
+     * from direct callers, or as part of a scheduled task.
+     * @p taskContext specifies whether the method is being
+     * called from within a task or not.
+     */
+    virtual void readFile(bool taskContext = false) = 0;
+    /*
+     * Writes the current state out to a file. This can happen
+     * from direct callers, or as part of a scheduled task.
+     * @p taskContext specifies whether the method is being
+     * called from within a task or not.
+     */
+    virtual void writeFile(bool taskContext = false) = 0;
+
+    /*
+     * Same method as above, but uses a QVariant so it can
+     * be called from Akonadi::ResourceScheduler.
+     */
+    virtual void writeFile(const QVariant &taskContext) = 0;
+
+protected:
+    /**
+     * Returns a pointer to the KConfig object which is used to store runtime
+     * information of the resource.
+     */
+    KSharedConfig::Ptr runtimeConfig() const;
+
+    /**
+     * Handles everything needed when the hash of a file has changed between the
+     * last write and the first read. This stores the new hash in a config file
+     * and notifies implementing resources to handle a hash change if the
+     * previous known hash was not empty. Finally this method clears the cache
+     * and calls synchronize.
+     * Returns true on succes, false otherwise.
+     */
+    bool readLocalFile(const QString &fileName);
+
+    /**
+     * Reimplement to read your data from the given file.
+     * The file is always local, loading from the network is done
+     * automatically if needed.
+     */
+    virtual bool readFromFile(const QString &fileName) = 0;
+
+    /**
+     * Reimplement to write your data to the given file.
+     * The file is always local, storing back to the network url is done
+     * automatically when needed.
+     */
+    virtual bool writeToFile(const QString &fileName) = 0;
+
+    /**
+     * It is not always needed to parse the file when a resources is started.
+     * (e.g. When the hash of the file is the same as the last time the resource
+     * has written changes to the file). In this case setActualFileName is
+     * called so that the implementing resource does know which file to read
+     * when it actually needs to read the file.
+     *
+     * The default implementation will just call readFromFile( fileName ), so
+     * implementing resources will have to explictly reimplement this method to
+     * actually get any profit of this.
+     *
+     * @p fileName This will always be a path to a local file.
+     */
+    virtual void setLocalFileName(const QString &fileName);
+
+    /**
+     * Generates the full path for the cache file in the case that a remote file
+     * is used.
+     */
+    QString cacheFile() const;
+
+    /**
+     * Calculates an MD5 hash for given file. If the file does not exists
+     * or the path is empty, this will return an empty QByteArray.
+     */
+    QByteArray calculateHash(const QString &fileName) const;
+
+    /**
+     * This method is called when the hash of the file has changed between the
+     * last writeFile() and a readFile() call. This means that the file was
+     * changed by another program.
+     *
+     * Note: This method is <em>not</em> called when the last known hash is
+     *       empty. In that case it is assumed that the file is loaded for the
+     *       first time.
+     */
+    virtual void handleHashChange();
+
+    /**
+     * Returns the hash that was stored to a cache file.
+     */
+    QByteArray loadHash() const;
+
+    /**
+     * Stores the given hash into a cache file.
+     */
+    void saveHash(const QByteArray &hash) const;
+
+    /**
+     * Returns whether the resource can be written to.
+     */
+    virtual bool readOnly() const = 0;
+
+    /**
+     * Returns the collection of this resource.
+     */
+    virtual Collection rootCollection() const = 0;
+
+protected:
+    QUrl mCurrentUrl;
+    QStringList mSupportedMimetypes;
+    QString mCollectionIcon;
+    KIO::FileCopyJob *mDownloadJob;
+    KIO::FileCopyJob *mUploadJob;
+    QByteArray mCurrentHash;
+
+protected Q_SLOTS:
+    void scheduleWrite(); /// Called when changes are added to the ChangeRecorder.
+
+private Q_SLOTS:
+    void handleProgress(KJob *, unsigned long);
+    void fileChanged(const QString &fileName);
+    void slotDownloadJobResult(KJob *);
+    void slotUploadJobResult(KJob *);
+};
+
+}
+
+#endif
diff --git a/resources/shared/singlefileresource/singlefileresourceconfigdialog.h b/resources/shared/singlefileresource/singlefileresourceconfigdialog.h
new file mode 100644 (file)
index 0000000..3b6695f
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+    Copyright (c) 2008 Bertjan Broeksema <b.broeksema@kdemail.org>
+    Copyright (c) 2008 Volker Krause <vkrause@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef AKONADI_SINGLEFILERESOURCECONFIGDIALOG_H
+#define AKONADI_SINGLEFILERESOURCECONFIGDIALOG_H
+
+#include "akonadi-singlefileresource_export.h"
+#include "singlefileresourceconfigdialogbase.h"
+
+#include <KConfigDialogManager>
+
+namespace Akonadi
+{
+
+/**
+ * Configuration dialog for single file resources.
+ */
+template <typename Settings>
+class AKONADI_SINGLEFILERESOURCE_EXPORT SingleFileResourceConfigDialog : public SingleFileResourceConfigDialogBase
+{
+    Settings *mSettings;
+
+public:
+    explicit SingleFileResourceConfigDialog(WId windowId, Settings *settings)
+        : SingleFileResourceConfigDialogBase(windowId)
+        , mSettings(settings)
+    {
+        ui.kcfg_Path->setUrl(QUrl::fromUserInput(mSettings->path()));
+        mManager = new KConfigDialogManager(this, mSettings);
+        mManager->updateWidgets();
+    }
+
+protected:
+    void save() Q_DECL_OVERRIDE {
+        mManager->updateSettings();
+        mSettings->setPath(ui.kcfg_Path->url().toString());
+        mSettings->save();
+    }
+};
+
+}
+
+#endif
diff --git a/resources/shared/singlefileresource/singlefileresourceconfigdialog.ui b/resources/shared/singlefileresource/singlefileresourceconfigdialog.ui
new file mode 100644 (file)
index 0000000..cd8f100
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SingleFileResourceConfigDialog</class>
+ <widget class="QWidget" name="SingleFileResourceConfigDialog">
+  <layout class="QGridLayout" name="gridLayout_2">
+   <item row="0" column="0">
+    <widget class="QTabWidget" name="tabWidget">
+    </widget>
+   </item>
+  </layout>
+ </widget>
+</ui>
diff --git a/resources/shared/singlefileresource/singlefileresourceconfigdialog_desktop.ui b/resources/shared/singlefileresource/singlefileresourceconfigdialog_desktop.ui
new file mode 100644 (file)
index 0000000..dec5686
--- /dev/null
@@ -0,0 +1,175 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SingleFileResourceConfigDialog</class>
+ <widget class="QWidget" name="SingleFileResourceConfigDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>487</width>
+    <height>497</height>
+   </rect>
+  </property>
+  <layout class="QGridLayout" name="gridLayout_2">
+   <item row="0" column="0">
+    <widget class="QTabWidget" name="tabWidget">
+     <property name="currentIndex">
+      <number>0</number>
+     </property>
+     <widget class="QWidget" name="tab">
+      <attribute name="title">
+       <string>File</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="tabLayout">
+       <item>
+        <widget class="QGroupBox" name="groupBox_2">
+         <property name="title">
+          <string>Filename</string>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout">
+          <item>
+           <layout class="QHBoxLayout" name="horizontalLayout">
+            <item>
+             <widget class="QLabel" name="label">
+              <property name="text">
+               <string>&amp;Filename:</string>
+              </property>
+              <property name="buddy">
+               <cstring>kcfg_Path</cstring>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="KUrlRequester" name="kcfg_Path"/>
+            </item>
+           </layout>
+          </item>
+          <item>
+           <widget class="QLabel" name="statusLabel">
+            <property name="text">
+             <string>Status:</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QLabel" name="label_3">
+            <property name="text">
+             <string>Select the file whose contents should be represented by this resource. If the file does not exist, it will be created. A URL of a remote file can also be specified, but note that monitoring for file changes will not work in this case.</string>
+            </property>
+            <property name="wordWrap">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <widget class="QGroupBox" name="groupBox_3">
+         <property name="title">
+          <string>Display Name</string>
+         </property>
+         <layout class="QVBoxLayout" name="verticalLayout_2">
+          <item>
+           <layout class="QHBoxLayout" name="horizontalLayout_2">
+            <item>
+             <widget class="QLabel" name="label_1">
+              <property name="text">
+               <string>&amp;Name:</string>
+              </property>
+              <property name="buddy">
+               <cstring>kcfg_DisplayName</cstring>
+              </property>
+             </widget>
+            </item>
+            <item>
+             <widget class="KLineEdit" name="kcfg_DisplayName"/>
+            </item>
+           </layout>
+          </item>
+          <item>
+           <widget class="QLabel" name="label_4">
+            <property name="text">
+             <string>Enter the name used to identify this resource in displays. If not specified, the filename will be used.</string>
+            </property>
+            <property name="wordWrap">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <widget class="QGroupBox" name="groupBox">
+         <property name="title">
+          <string>Access Rights</string>
+         </property>
+         <layout class="QGridLayout" name="gridLayout_4">
+          <item row="0" column="0">
+           <widget class="QCheckBox" name="kcfg_ReadOnly">
+            <property name="text">
+             <string>Read only</string>
+            </property>
+           </widget>
+          </item>
+          <item row="1" column="0">
+           <widget class="QLabel" name="label_2">
+            <property name="text">
+             <string>If read-only mode is enabled, no changes will be written to the file selected above. Read-only mode will be automatically enabled if you do not have write access to the file or the file is on a remote server that does not support write access.</string>
+            </property>
+            <property name="wordWrap">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item>
+        <widget class="QGroupBox" name="groupBox_MonitorFile">
+         <property name="title">
+          <string>Monitoring</string>
+         </property>
+         <layout class="QGridLayout" name="gridLayout">
+          <item row="1" column="0" colspan="3">
+           <widget class="QLabel" name="label_5">
+            <property name="text">
+             <string>If file monitoring is enabled the resource will reload the file when changes are made by other programs. It also tries to create a backup in case of conflicts whenever possible.</string>
+            </property>
+            <property name="wordWrap">
+             <bool>true</bool>
+            </property>
+           </widget>
+          </item>
+          <item row="0" column="0">
+           <widget class="QCheckBox" name="kcfg_MonitorFile">
+            <property name="text">
+             <string>Enable file &amp;monitoring</string>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>KLineEdit</class>
+   <extends>QLineEdit</extends>
+   <header>klineedit.h</header>
+  </customwidget>
+  <customwidget>
+   <class>KUrlRequester</class>
+   <extends>QFrame</extends>
+   <header>kurlrequester.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/resources/shared/singlefileresource/singlefileresourceconfigdialogbase.cpp b/resources/shared/singlefileresource/singlefileresourceconfigdialogbase.cpp
new file mode 100644 (file)
index 0000000..2103de1
--- /dev/null
@@ -0,0 +1,233 @@
+/*
+    Copyright (c) 2008 Bertjan Broeksema <b.broeksema@kdemail.org>
+    Copyright (c) 2008 Volker Krause <vkrause@kde.org>
+    Copyright (c) 2010,2011 David Jarvie <djarvie@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "singlefileresourceconfigdialogbase.h"
+
+#include <QTabWidget>
+#include <KConfigDialogManager>
+#include <KFileItem>
+#include <KIO/Job>
+#include <KWindowSystem>
+#include <QUrl>
+#include <QTimer>
+
+#include <KLocalizedString>
+#include <KSharedConfig>
+#include <QDialogButtonBox>
+#include <KConfigGroup>
+#include <QPushButton>
+#include <QVBoxLayout>
+
+using namespace Akonadi;
+
+SingleFileResourceConfigDialogBase::SingleFileResourceConfigDialogBase(WId windowId) :
+    QDialog(),
+    mManager(Q_NULLPTR),
+    mStatJob(Q_NULLPTR),
+    mAppendedWidget(Q_NULLPTR),
+    mDirUrlChecked(false),
+    mMonitorEnabled(true),
+    mLocalFileOnly(false)
+{
+    QWidget *mainWidget = new QWidget(this);
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    setLayout(mainLayout);
+    mainLayout->addWidget(mainWidget);
+    ui.setupUi(mainWidget);
+    ui.kcfg_Path->setMode(KFile::File);
+    ui.statusLabel->setText(QString());
+
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    mOkButton = buttonBox->button(QDialogButtonBox::Ok);
+    mOkButton->setDefault(true);
+    mOkButton->setShortcut(Qt::CTRL | Qt::Key_Return);
+    connect(buttonBox, &QDialogButtonBox::accepted, this, &SingleFileResourceConfigDialogBase::accept);
+    connect(buttonBox, &QDialogButtonBox::rejected, this, &SingleFileResourceConfigDialogBase::reject);
+    mainLayout->addWidget(buttonBox);
+
+    if (windowId) {
+        KWindowSystem::setMainWindow(this, windowId);
+    }
+
+    ui.tabWidget->tabBar()->hide();
+
+    connect(mOkButton, &QPushButton::clicked, this, &SingleFileResourceConfigDialogBase::save);
+
+    connect(ui.kcfg_Path, &KUrlRequester::textChanged, this, &SingleFileResourceConfigDialogBase::validate);
+    connect(ui.kcfg_MonitorFile, &QCheckBox::toggled, this, &SingleFileResourceConfigDialogBase::validate);
+    ui.kcfg_Path->setFocus();
+    QTimer::singleShot(0, this, &SingleFileResourceConfigDialogBase::validate);
+    setMinimumSize(600, 540);
+    readConfig();
+}
+
+SingleFileResourceConfigDialogBase::~SingleFileResourceConfigDialogBase()
+{
+    writeConfig();
+}
+
+void SingleFileResourceConfigDialogBase::writeConfig()
+{
+    KConfigGroup group(KSharedConfig::openConfig(), "SingleFileResourceConfigDialogBase");
+    group.writeEntry("Size", size());
+}
+
+void SingleFileResourceConfigDialogBase::readConfig()
+{
+    KConfigGroup group(KSharedConfig::openConfig(), "SingleFileResourceConfigDialogBase");
+    const QSize sizeDialog = group.readEntry("Size", QSize(600, 540));
+    if (sizeDialog.isValid()) {
+        resize(sizeDialog);
+    }
+}
+
+void SingleFileResourceConfigDialogBase::addPage(const QString &title, QWidget *page)
+{
+    ui.tabWidget->tabBar()->show();
+    ui.tabWidget->addTab(page, title);
+    mManager->addWidget(page);
+    mManager->updateWidgets();
+}
+
+void SingleFileResourceConfigDialogBase::setFilter(const QString &filter)
+{
+    ui.kcfg_Path->setFilter(filter);
+}
+
+void SingleFileResourceConfigDialogBase::setMonitorEnabled(bool enable)
+{
+    mMonitorEnabled = enable;
+    ui.groupBox_MonitorFile->setVisible(mMonitorEnabled);
+}
+
+void SingleFileResourceConfigDialogBase::setUrl(const QUrl &url)
+{
+    ui.kcfg_Path->setUrl(url);
+}
+
+QUrl SingleFileResourceConfigDialogBase::url() const
+{
+    return ui.kcfg_Path->url();
+}
+
+void SingleFileResourceConfigDialogBase::setLocalFileOnly(bool local)
+{
+    mLocalFileOnly = local;
+    ui.kcfg_Path->setMode(mLocalFileOnly ? KFile::File | KFile::LocalOnly : KFile::File);
+}
+
+void SingleFileResourceConfigDialogBase::appendWidget(SingleFileValidatingWidget *widget)
+{
+    widget->setParent(static_cast<QWidget *>(ui.tab));
+    ui.tabLayout->addWidget(widget);
+    connect(widget, &SingleFileValidatingWidget::changed, this, &SingleFileResourceConfigDialogBase::validate);
+    mAppendedWidget = widget;
+}
+
+void SingleFileResourceConfigDialogBase::validate()
+{
+    if (mAppendedWidget && !mAppendedWidget->validate()) {
+        mOkButton->setEnabled(false);
+        return;
+    }
+
+    const QUrl currentUrl = ui.kcfg_Path->url();
+    if (ui.kcfg_Path->text().trimmed().isEmpty() || currentUrl.isEmpty()) {
+        mOkButton->setEnabled(false);
+        return;
+    }
+
+    if (currentUrl.isLocalFile()) {
+        if (mMonitorEnabled) {
+            ui.kcfg_MonitorFile->setEnabled(true);
+        }
+        ui.statusLabel->setText(QString());
+
+        // The read-only checkbox used to be disabled if the file is read-only,
+        // but it is then impossible to know at a later date if the file
+        // permissions change, whether the user actually wanted the resource to be
+        // read-only or not. So just leave the read-only checkbox untouched.
+        mOkButton->setEnabled(true);
+    } else {
+        // Not a local file.
+        if (mLocalFileOnly) {
+            mOkButton->setEnabled(false);
+            return;
+        }
+        if (mMonitorEnabled) {
+            ui.kcfg_MonitorFile->setEnabled(false);
+        }
+        ui.statusLabel->setText(i18nc("@info:status", "Checking file information..."));
+
+        if (mStatJob) {
+            mStatJob->kill();
+        }
+
+        mStatJob = KIO::stat(currentUrl, KIO::DefaultFlags | KIO::HideProgressInfo);
+        mStatJob->setDetails(2);   // All details.
+        mStatJob->setSide(KIO::StatJob::SourceSide);
+
+        connect(mStatJob, &KIO::StatJob::result, this, &SingleFileResourceConfigDialogBase::slotStatJobResult);
+
+        // Allow the OK button to be disabled until the MetaJob is finished.
+        mOkButton->setEnabled(false);
+    }
+}
+
+void SingleFileResourceConfigDialogBase::slotStatJobResult(KJob *job)
+{
+    if (job->error() == KIO::ERR_DOES_NOT_EXIST && !mDirUrlChecked) {
+        // The file did not exist, so let's see if the directory the file should
+        // reside in supports writing.
+
+        QUrl dirUrl(ui.kcfg_Path->url());
+        dirUrl = KIO::upUrl(dirUrl);
+
+        mStatJob = KIO::stat(dirUrl, KIO::DefaultFlags | KIO::HideProgressInfo);
+        mStatJob->setDetails(2);   // All details.
+        mStatJob->setSide(KIO::StatJob::SourceSide);
+
+        connect(mStatJob, &KIO::StatJob::result, this, &SingleFileResourceConfigDialogBase::slotStatJobResult);
+
+        // Make sure we don't check the whole path upwards.
+        mDirUrlChecked = true;
+        return;
+    } else if (job->error()) {
+        // It doesn't seem possible to read nor write from the location so leave the
+        // ok button disabled
+        ui.statusLabel->setText(QString());
+        mOkButton->setEnabled(false);
+        mDirUrlChecked = false;
+        mStatJob = Q_NULLPTR;
+        return;
+    }
+
+    ui.statusLabel->setText(QString());
+    mOkButton->setEnabled(true);
+
+    mDirUrlChecked = false;
+    mStatJob = Q_NULLPTR;
+}
+
+SingleFileValidatingWidget::SingleFileValidatingWidget(QWidget *parent)
+    : QWidget(parent)
+{
+}
diff --git a/resources/shared/singlefileresource/singlefileresourceconfigdialogbase.h b/resources/shared/singlefileresource/singlefileresourceconfigdialogbase.h
new file mode 100644 (file)
index 0000000..db6e43b
--- /dev/null
@@ -0,0 +1,146 @@
+/*
+    Copyright (c) 2008 Bertjan Broeksema <b.broeksema@kdemail.org>
+    Copyright (c) 2008 Volker Krause <vkrause@kde.org>
+    Copyright (c) 2010,2011 David Jarvie <djarvie@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef AKONADI_SINGLEFILERESOURCECONFIGDIALOGBASE_H
+#define AKONADI_SINGLEFILERESOURCECONFIGDIALOGBASE_H
+
+#include "akonadi-singlefileresource_export.h"
+
+#include "ui_singlefileresourceconfigdialog_desktop.h"
+
+#include <QDialog>
+#include <QUrl>
+class KConfigDialogManager;
+class KJob;
+class QPushButton;
+
+namespace KIO
+{
+class StatJob;
+}
+
+namespace Akonadi
+{
+
+class SingleFileValidatingWidget;
+
+/**
+ * Base class for the configuration dialog for single file based resources.
+ * @see SingleFileResourceConfigDialog
+ */
+class AKONADI_SINGLEFILERESOURCE_EXPORT SingleFileResourceConfigDialogBase : public QDialog
+{
+    Q_OBJECT
+public:
+    explicit SingleFileResourceConfigDialogBase(WId windowId);
+    ~SingleFileResourceConfigDialogBase();
+
+    /**
+     * Adds @param page to the tabwidget. This can be used to add custom
+     * settings for a specific single file resource.
+     */
+    void addPage(const QString &title, QWidget *page);
+
+    /**
+     * Set file extension filter.
+     */
+    void setFilter(const QString &filter);
+
+    /**
+     * Enable and show, or disable and hide, the monitor option.
+     * If the option is disabled, its value will not be saved.
+     * By default, the monitor option is enabled.
+     */
+    void setMonitorEnabled(bool enable);
+
+    /**
+     * Return the file URL.
+     */
+    QUrl url() const;
+
+    /**
+     * Set the file URL.
+     */
+    void setUrl(const QUrl &url);
+
+    /**
+     * Specify whether the file must be local.
+     * The default is to allow both local and remote files.
+     */
+    void setLocalFileOnly(bool local);
+
+    /**
+     * Add a widget to the dialog.
+     */
+    void appendWidget(SingleFileValidatingWidget *widget);
+
+protected Q_SLOTS:
+    virtual void save() = 0;
+
+protected:
+    Ui::SingleFileResourceConfigDialog ui;
+    KConfigDialogManager *mManager;
+
+private Q_SLOTS:
+    void validate();
+    void slotStatJobResult(KJob *);
+
+private:
+    void writeConfig();
+    void readConfig();
+    KIO::StatJob *mStatJob;
+    SingleFileValidatingWidget *mAppendedWidget;
+    bool mDirUrlChecked;
+    bool mMonitorEnabled;
+    bool mLocalFileOnly;
+    QPushButton *mOkButton;
+};
+
+/**
+ * Base class for widgets added to SingleFileResourceConfigDialogBase
+ * using its appendWidget() method.
+ *
+ * Derived classes must implement validate() andQ_EMIT changed() when
+ * appropriate.
+ */
+class AKONADI_SINGLEFILERESOURCE_EXPORT SingleFileValidatingWidget : public QWidget
+{
+    Q_OBJECT
+public:
+    explicit SingleFileValidatingWidget(QWidget *parent = Q_NULLPTR);
+
+    /**
+     * Return whether the widget's value is valid when the dialog is
+     * accepted.
+     */
+    virtual bool validate() const = 0;
+
+Q_SIGNALS:
+    /**
+     * Signal emitted when the widget's value changes in a way which
+     * might affect the result of validate().
+     */
+    void changed();
+};
+
+}
+
+#endif
diff --git a/resources/vcard/CMakeLists.txt b/resources/vcard/CMakeLists.txt
new file mode 100644 (file)
index 0000000..4a2616e
--- /dev/null
@@ -0,0 +1,36 @@
+
+
+if(BUILD_TESTING)
+    add_subdirectory( autotests )
+endif()
+
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_vcard_resource\")
+
+########### next target ###############
+
+set( vcardresource_SRCS
+  vcardresource.cpp
+)
+
+install( FILES vcardresource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents" )
+
+kconfig_add_kcfg_files(vcardresource_SRCS settings.kcfgc)
+kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/vcardresource.kcfg org.kde.Akonadi.VCard.Settings)
+qt5_add_dbus_adaptor(vcardresource_SRCS
+  ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.VCard.Settings.xml settings.h Akonadi_VCard_Resource::Settings vcardsettingsadaptor VCardSettingsAdaptor
+)
+
+add_executable(akonadi_vcard_resource ${vcardresource_SRCS})
+
+add_subdirectory( wizard )
+
+if( APPLE )
+  set_target_properties(akonadi_vcard_resource PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../Info.plist.template)
+  set_target_properties(akonadi_vcard_resource PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.VCard")
+  set_target_properties(akonadi_vcard_resource PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi VCard Resource")
+endif ()
+
+target_link_libraries(akonadi_vcard_resource KF5::AkonadiCore KF5::KIOCore   KF5::AkonadiAgentBase KF5::Contacts KF5::DBusAddons akonadi-singlefileresource KF5::Completion KF5::KIOWidgets KF5::ConfigWidgets KF5::KDELibs4Support)
+
+install(TARGETS akonadi_vcard_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
+
diff --git a/resources/vcard/Messages.sh b/resources/vcard/Messages.sh
new file mode 100644 (file)
index 0000000..a766b04
--- /dev/null
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+$EXTRACTRC `find . -name \*.ui` `find . -name \*.kcfg` >> rc.cpp || exit 11
+$XGETTEXT *.cpp -o $podir/akonadi_vcard_resource.pot
diff --git a/resources/vcard/autotests/CMakeLists.txt b/resources/vcard/autotests/CMakeLists.txt
new file mode 100644 (file)
index 0000000..eb78b92
--- /dev/null
@@ -0,0 +1,6 @@
+configure_file(${CMAKE_CURRENT_SOURCE_DIR}/vcardtest.xml ${CMAKE_CURRENT_BINARY_DIR}/vcardtest.xml COPYONLY)
+configure_file(${CMAKE_CURRENT_SOURCE_DIR}/vcardtest-readonly.xml ${CMAKE_CURRENT_BINARY_DIR}/vcardtest-readonly.xml COPYONLY)
+configure_file(${CMAKE_CURRENT_SOURCE_DIR}/vcardtest.vcf ${CMAKE_CURRENT_BINARY_DIR}/vcardtest.vcf COPYONLY)
+
+akonadi_add_resourcetest( vcard-read vcardtest.js )
+akonadi_add_resourcetest( vcard-read-readonly vcardtest-readonly.js )
diff --git a/resources/vcard/autotests/vcardtest-readonly.js b/resources/vcard/autotests/vcardtest-readonly.js
new file mode 100644 (file)
index 0000000..b776b80
--- /dev/null
@@ -0,0 +1,12 @@
+Resource.setType( "akonadi_vcard_resource" );
+Resource.setPathOption( "Path", "vcardtest.vcf" );
+Resource.setOption( "ReadOnly", true );
+Resource.create();
+
+XmlOperations.setXmlFile( "vcardtest-readonly.xml" );
+XmlOperations.setRootCollections( Resource.identifier() );
+XmlOperations.setCollectionKey( "None" ); // we only expect one collection
+XmlOperations.ignoreCollectionField( "Name" ); // name is the resource identifier and thus unpredictable
+XmlOperations.ignoreCollectionField( "RemoteId" ); // remote id is the absolute path
+XmlOperations.assertEqual();
+
diff --git a/resources/vcard/autotests/vcardtest-readonly.xml b/resources/vcard/autotests/vcardtest-readonly.xml
new file mode 100644 (file)
index 0000000..7f6d3b2
--- /dev/null
@@ -0,0 +1,24 @@
+<knut>
+  <collection rid="vcardtest.vcf" name="akonadi_vcard_resource_0" content="text/directory">
+    <attribute type="AccessRights" >W</attribute>
+    <attribute type="ENTITYDISPLAY" >("vcardtest.vcf" "office-address-book")</attribute>
+    <item rid="bb2slGmqxb" mimetype="text/directory">
+      <payload>
+BEGIN:VCARD
+EMAIL:vkrause@kde.org
+FN:Volker Krause
+GEO:52.500000;13.366667
+N:Krause;Volker;;;
+NAME:Volker Krause
+ORG:KDE
+REV:2003-02-27T20:08:42Z
+ROLE:Author of this file
+TZ:+02:00
+UID:bb2slGmqxb
+URL:http://www.akonadi-project.org
+VERSION:3.0
+END:VCARD
+      </payload>
+    </item>
+  </collection>
+</knut>
diff --git a/resources/vcard/autotests/vcardtest.js b/resources/vcard/autotests/vcardtest.js
new file mode 100644 (file)
index 0000000..5be52ff
--- /dev/null
@@ -0,0 +1,11 @@
+Resource.setType( "akonadi_vcard_resource" );
+Resource.setPathOption( "Path", "vcardtest.vcf" );
+Resource.create();
+
+XmlOperations.setXmlFile( "vcardtest.xml" );
+XmlOperations.setRootCollections( Resource.identifier() );
+XmlOperations.setCollectionKey( "None" ); // we only expect one collection
+XmlOperations.ignoreCollectionField( "Name" ); // name is the resource identifier and thus unpredictable
+XmlOperations.ignoreCollectionField( "RemoteId" ); // remote id is the absolute path
+XmlOperations.assertEqual();
+
diff --git a/resources/vcard/autotests/vcardtest.vcf b/resources/vcard/autotests/vcardtest.vcf
new file mode 100644 (file)
index 0000000..0041488
--- /dev/null
@@ -0,0 +1,15 @@
+BEGIN:VCARD
+EMAIL:vkrause@kde.org
+FN:Volker Krause
+GEO:52.500000;13.366667
+N:Krause;Volker;;;
+NAME:Volker Krause
+ORG:KDE
+REV:2003-02-27T20:08:42Z
+ROLE:Author of this file
+TZ:+02:00
+UID:bb2slGmqxb
+URL:http://www.akonadi-project.org
+VERSION:3.0
+END:VCARD
+
diff --git a/resources/vcard/autotests/vcardtest.xml b/resources/vcard/autotests/vcardtest.xml
new file mode 100644 (file)
index 0000000..b226de2
--- /dev/null
@@ -0,0 +1,24 @@
+<knut>
+  <collection rid="vcardtest.vcf" name="akonadi_vcard_resource_0" content="text/directory">
+    <attribute type="AccessRights" >wcdW</attribute>
+    <attribute type="ENTITYDISPLAY" >("vcardtest.vcf" "office-address-book")</attribute>
+    <item rid="bb2slGmqxb" mimetype="text/directory">
+      <payload>
+BEGIN:VCARD
+EMAIL:vkrause@kde.org
+FN:Volker Krause
+GEO:52.500000;13.366667
+N:Krause;Volker;;;
+NAME:Volker Krause
+ORG:KDE
+REV:2003-02-27T20:08:42Z
+ROLE:Author of this file
+TZ:+02:00
+UID:bb2slGmqxb
+URL:http://www.akonadi-project.org
+VERSION:3.0
+END:VCARD
+      </payload>
+    </item>
+  </collection>
+</knut>
diff --git a/resources/vcard/settings.kcfgc b/resources/vcard/settings.kcfgc
new file mode 100644 (file)
index 0000000..a68977b
--- /dev/null
@@ -0,0 +1,9 @@
+File=vcardresource.kcfg
+ClassName=Settings
+Mutators=true
+ItemAccessors=true
+SetUserTexts=true
+Singleton=false
+#IncludeFiles=
+GlobalEnums=true
+NameSpace=Akonadi_VCard_Resource
diff --git a/resources/vcard/vcardresource.cpp b/resources/vcard/vcardresource.cpp
new file mode 100644 (file)
index 0000000..c2b26c4
--- /dev/null
@@ -0,0 +1,196 @@
+/*
+    Copyright (c) 2007 Tobias Koenig <tokoe@kde.org>
+    Copyright (c) 2008 Bertjan Broeksema <broeksema@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "vcardresource.h"
+#include "vcardsettingsadaptor.h"
+#include "singlefileresourceconfigdialog.h"
+
+#include <kdbusconnectionpool.h>
+
+#include <KLocalizedString>
+
+#include <QtDBus/QDBusConnection>
+
+using namespace Akonadi;
+using namespace Akonadi_VCard_Resource;
+
+VCardResource::VCardResource(const QString &id)
+    : SingleFileResource<Settings>(id)
+{
+    setSupportedMimetypes(QStringList() << KContacts::Addressee::mimeType(), QStringLiteral("office-address-book"));
+
+    new VCardSettingsAdaptor(mSettings);
+    KDBusConnectionPool::threadConnection().registerObject(QStringLiteral("/Settings"),
+            mSettings, QDBusConnection::ExportAdaptors);
+}
+
+VCardResource::~VCardResource()
+{
+    mAddressees.clear();
+}
+
+bool VCardResource::retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts)
+{
+    Q_UNUSED(parts);
+    const QString rid = item.remoteId();
+    if (!mAddressees.contains(rid)) {
+        Q_EMIT error(i18n("Contact with uid '%1' not found.", rid));
+        return false;
+    }
+    Item i(item);
+    i.setPayload<KContacts::Addressee>(mAddressees.value(rid));
+    itemRetrieved(i);
+    return true;
+}
+
+void VCardResource::aboutToQuit()
+{
+    if (!mSettings->readOnly()) {
+        writeFile();
+    }
+    mSettings->save();
+}
+
+void VCardResource::customizeConfigDialog(SingleFileResourceConfigDialog<Settings> *dlg)
+{
+    dlg->setWindowIcon(QIcon::fromTheme(QStringLiteral("text-directory")));
+    dlg->setFilter(QStringLiteral("*.vcf|") + i18nc("Filedialog filter for *.vcf", "vCard Address Book File"));
+    dlg->setWindowTitle(i18n("Select Address Book"));
+}
+
+void VCardResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &)
+{
+    KContacts::Addressee addressee;
+    if (item.hasPayload<KContacts::Addressee>()) {
+        addressee  = item.payload<KContacts::Addressee>();
+    }
+
+    if (!addressee.isEmpty()) {
+        mAddressees.insert(addressee.uid(), addressee);
+
+        Item i(item);
+        i.setRemoteId(addressee.uid());
+        changeCommitted(i);
+
+        scheduleWrite();
+    } else {
+        changeProcessed();
+    }
+}
+
+void VCardResource::itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &)
+{
+    KContacts::Addressee addressee;
+    if (item.hasPayload<KContacts::Addressee>()) {
+        addressee  = item.payload<KContacts::Addressee>();
+    }
+
+    if (!addressee.isEmpty()) {
+        mAddressees.insert(addressee.uid(), addressee);
+
+        Item i(item);
+        i.setRemoteId(addressee.uid());
+        changeCommitted(i);
+
+        scheduleWrite();
+    } else {
+        changeProcessed();
+    }
+}
+
+void VCardResource::itemRemoved(const Akonadi::Item &item)
+{
+    if (mAddressees.contains(item.remoteId())) {
+        mAddressees.remove(item.remoteId());
+    }
+
+    scheduleWrite();
+
+    changeProcessed();
+}
+
+void VCardResource::retrieveItems(const Akonadi::Collection &col)
+{
+    // VCard does not support folders so we can safely ignore the collection
+    Q_UNUSED(col);
+
+    Item::List items;
+    items.reserve(mAddressees.count());
+
+    // FIXME: Check if the KIO::Job is done and was successful, if so send the
+    // items, otherwise set a bool and in the result slot of the job send the
+    // items if the bool is set.
+
+    foreach (const KContacts::Addressee &addressee, mAddressees) {
+        Item item;
+        item.setRemoteId(addressee.uid());
+        item.setMimeType(KContacts::Addressee::mimeType());
+        item.setPayload(addressee);
+        items.append(item);
+    }
+
+    itemsRetrieved(items);
+}
+
+bool VCardResource::readFromFile(const QString &fileName)
+{
+    mAddressees.clear();
+
+    QFile file(QUrl::fromLocalFile(fileName).toLocalFile());
+    if (!file.open(QIODevice::ReadOnly)) {
+        Q_EMIT status(Broken, i18n("Unable to open vCard file '%1'.", fileName));
+        return false;
+    }
+
+    const QByteArray data = file.readAll();
+    file.close();
+
+    const KContacts::Addressee::List list = mConverter.parseVCards(data);
+    const int numberOfElementInList = list.count();
+    for (int i = 0; i < numberOfElementInList; ++i) {
+        mAddressees.insert(list[ i ].uid(), list[ i ]);
+    }
+
+    return true;
+}
+
+bool VCardResource::writeToFile(const QString &fileName)
+{
+    QFile file(fileName);
+    if (!file.open(QIODevice::WriteOnly)) {
+        Q_EMIT status(Broken, i18n("Unable to open vCard file '%1'.", fileName));
+        return false;
+    }
+
+    QVector<KContacts::Addressee> v;
+    v.reserve(mAddressees.size());
+    foreach (const KContacts::Addressee &addressee, mAddressees) {
+        v.push_back(addressee);
+    }
+
+    const QByteArray data = mConverter.createVCards(v);
+
+    file.write(data);
+    file.close();
+
+    return true;
+}
+
+AKONADI_RESOURCE_MAIN(VCardResource)
diff --git a/resources/vcard/vcardresource.desktop b/resources/vcard/vcardresource.desktop
new file mode 100644 (file)
index 0000000..a8c24a0
--- /dev/null
@@ -0,0 +1,91 @@
+[Desktop Entry]
+Name=vCard File
+Name[bg]=Файл vCard
+Name[bs]=VCard datoteka
+Name[ca]=Fitxer vCard
+Name[ca@valencia]=Fitxer vCard
+Name[cs]=Soubor s vizitkou
+Name[da]=vCard-fil
+Name[de]=vCard-Datei
+Name[el]=VCard αρχείο
+Name[en_GB]=vCard File
+Name[es]=Archivo vCard
+Name[et]=vCard-fail
+Name[fi]=vCard-tiedosto
+Name[fr]=Fichier « vCard »
+Name[ga]=Comhad v-Chárta
+Name[gl]=Ficheiro vCard
+Name[hu]=vCard fájl
+Name[ia]=File VCard
+Name[it]=File vCard
+Name[kk]=VCard файлы
+Name[ko]=vCard 파일
+Name[lt]=VCard failas
+Name[nb]=vCard-fil
+Name[nds]=VCard-Datei
+Name[nl]=vCard-bestand
+Name[pl]=Plik vCard
+Name[pt]=Ficheiro vCard
+Name[pt_BR]=Arquivo vCard
+Name[ru]=Файл vCard
+Name[sk]=Súbor VCard
+Name[sl]=Datoteka vCard
+Name[sr]=В‑кард фајл
+Name[sr@ijekavian]=В‑кард фајл
+Name[sr@ijekavianlatin]=VCard fajl
+Name[sr@latin]=VCard fajl
+Name[sv]=vCard-fil
+Name[tr]=VCard Dosyası
+Name[ug]=vCard ھۆججىتى
+Name[uk]=Файл vCard
+Name[x-test]=xxvCard Filexx
+Name[zh_CN]=vCard 文件
+Name[zh_TW]=vCard 檔案
+Comment=Loads data from a vCard file
+Comment[bg]=Зареждане на данни от файл vCard
+Comment[bs]=Učitava podatke iz VCard datoteke
+Comment[ca]=Carrega les dades des d'un fitxer vCard
+Comment[ca@valencia]=Carrega les dades des d'un fitxer vCard
+Comment[cs]=Načítá data ze souboru vizitek
+Comment[da]=Indlæser data fra en vCard-fil
+Comment[de]=Daten werden aus einer vCard-Datei geladen
+Comment[el]=Φορτώνει δεδομένα από ένα αρχείο VCard
+Comment[en_GB]=Loads data from a vCard file
+Comment[es]=Carga datos de un archivo vCard
+Comment[et]=Andmete laadimine vCard-failist
+Comment[fi]=Lataa tietoa vCard-tiedostosta
+Comment[fr]=Charge des données depuis un fichier au format « vCard »
+Comment[ga]=Breiseán a luchtaíonn sonraí ó chomhad v-Chárta
+Comment[gl]=Carga datos desde un ficheiro VCard
+Comment[hu]=Adatokat tölt be egy vCard fájlból
+Comment[ia]=Carga datos ex un file vCard
+Comment[it]=Carica dati da un file vCard
+Comment[kk]=VCard файлынан деректі алып береді
+Comment[ko]=vCard 파일에서 데이터를 가져옵니다
+Comment[lt]=Įkelia duomenis iš VCard failo
+Comment[nb]=Laster data fra en vCard-fil
+Comment[nds]=Laadt Daten ut en VCard-Datei
+Comment[nl]=Laadt gegevens van een vCard-bestand
+Comment[pl]=Wczytuje dane z plików vCard
+Comment[pt]=Carrega os dados de um ficheiro vCard
+Comment[pt_BR]=Carrega os dados de um arquivo vCard
+Comment[ru]=Загрузка данных из файла vCard
+Comment[sk]=Načíta dáta zo súboru vCard
+Comment[sl]=Naloži podatke iz datoteke vCard
+Comment[sr]=Учитава податке из в‑кард фајла
+Comment[sr@ijekavian]=Учитава податке из в‑кард фајла
+Comment[sr@ijekavianlatin]=Učitava podatke iz vCard fajla
+Comment[sr@latin]=Učitava podatke iz vCard fajla
+Comment[sv]=Laddar data från en vCard-fil
+Comment[tr]=Bir VCard dosyasından veri yükler
+Comment[uk]=Завантажує дані з файла vCard
+Comment[x-test]=xxLoads data from a vCard filexx
+Comment[zh_CN]=从 vCard 文件载入数据
+Comment[zh_TW]=從 vCard 檔載入資料
+Type=AkonadiResource
+Exec=akonadi_vcard_resource
+Icon=text-directory
+
+X-Akonadi-MimeTypes=text/directory
+X-Akonadi-Capabilities=Resource
+X-Akonadi-Identifier=akonadi_vcard_resource
diff --git a/resources/vcard/vcardresource.h b/resources/vcard/vcardresource.h
new file mode 100644 (file)
index 0000000..a85cf1d
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+    Copyright (c) 2007 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef VCARDRESOURCE_H
+#define VCARDRESOURCE_H
+
+#include "singlefileresource.h"
+#include "settings.h"
+
+#include <kcontacts/addressee.h>
+#include <kcontacts/vcardconverter.h>
+
+class VCardResource : public Akonadi::SingleFileResource<Akonadi_VCard_Resource::Settings>
+{
+    Q_OBJECT
+
+public:
+    explicit VCardResource(const QString &id);
+    ~VCardResource();
+
+protected Q_SLOTS:
+    bool retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+    void retrieveItems(const Akonadi::Collection &col) Q_DECL_OVERRIDE;
+
+protected:
+    /**
+     * Customize the configuration dialog before it is displayed.
+     */
+    void customizeConfigDialog(Akonadi::SingleFileResourceConfigDialog<Akonadi_VCard_Resource::Settings> *dlg) Q_DECL_OVERRIDE;
+
+    bool readFromFile(const QString &fileName) Q_DECL_OVERRIDE;
+    bool writeToFile(const QString &fileName) Q_DECL_OVERRIDE;
+    void aboutToQuit() Q_DECL_OVERRIDE;
+
+    void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    void itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+    void itemRemoved(const Akonadi::Item &item) Q_DECL_OVERRIDE;
+
+private:
+    QMap<QString, KContacts::Addressee> mAddressees;
+    KContacts::VCardConverter mConverter;
+};
+
+#endif
diff --git a/resources/vcard/vcardresource.kcfg b/resources/vcard/vcardresource.kcfg
new file mode 100644 (file)
index 0000000..0d31684
--- /dev/null
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:kcfg="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
+      http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
+  <kcfgfile arg="true" />
+  <group name="General">
+    <entry name="Path" type="Path">
+      <label>Path to vCard file.</label>
+      <default></default>
+    </entry>
+    <entry name="DisplayName" type="String">
+      <label>Display name.</label>
+      <default></default>
+    </entry>
+    <entry name="ReadOnly" type="Bool">
+      <label>Do not change the actual backend data.</label>
+      <default>false</default>
+    </entry>
+    <entry name="MonitorFile" type="Bool">
+      <label>Monitor file for changes.</label>
+      <default>true</default>
+    </entry>
+  </group>
+</kcfg>
diff --git a/resources/vcard/wizard/CMakeLists.txt b/resources/vcard/wizard/CMakeLists.txt
new file mode 100644 (file)
index 0000000..879d755
--- /dev/null
@@ -0,0 +1,4 @@
+set(VCARD_FILE_DEFAULT_PATH "$HOME/.local/share/kaddressbook/contact.vcf")
+
+configure_file(vcardwizard.es.cmake ${CMAKE_CURRENT_BINARY_DIR}/vcardwizard.es)
+install ( FILES vcardwizard.desktop ${CMAKE_CURRENT_BINARY_DIR}/vcardwizard.es vcardwizard.ui DESTINATION ${KDE_INSTALL_DATADIR}/akonadi/accountwizard/vcard )
diff --git a/resources/vcard/wizard/Messages.sh b/resources/vcard/wizard/Messages.sh
new file mode 100644 (file)
index 0000000..e3657ed
--- /dev/null
@@ -0,0 +1,4 @@
+#! /usr/bin/env bash
+$EXTRACTRC *.ui >> rc.cpp
+$XGETTEXT *.cpp -o $podir/accountwizard_vcard.pot
+$XGETTEXT -kqsTr *.es.cmake -j -o $podir/accountwizard_vcard.pot
diff --git a/resources/vcard/wizard/vcardwizard.desktop b/resources/vcard/wizard/vcardwizard.desktop
new file mode 100644 (file)
index 0000000..68b5546
--- /dev/null
@@ -0,0 +1,82 @@
+[Desktop Entry]
+Name=VCard File
+Name[bg]=Файл vCard
+Name[ca]=Fitxer vCard
+Name[ca@valencia]=Fitxer vCard
+Name[cs]=Soubor s vizitkou
+Name[da]=VCard-fil
+Name[de]=vCard-Datei
+Name[el]=VCard αρχείο
+Name[en_GB]=VCard File
+Name[es]=Archivo VCard
+Name[et]=vCard-fail
+Name[fi]=VCard-tiedosto
+Name[fr]=Fichier « vCard »
+Name[gl]=Ficheiro VCard
+Name[hu]=VCard fájl
+Name[it]=File vCard
+Name[ja]=VCard ファイル
+Name[ko]=vCard 파일
+Name[nb]=vCard-fil
+Name[nds]=vCard-Datei
+Name[nl]=VCard-bestand
+Name[nn]=vCard-fil
+Name[pl]=Plik VCard
+Name[pt]=Ficheiro vCard
+Name[pt_BR]=Arquivo vCard
+Name[ru]=Файл vCard
+Name[sk]=Súbor VCard
+Name[sl]=Datoteka vCard
+Name[sr]=В‑кард фајл
+Name[sr@ijekavian]=В‑кард фајл
+Name[sr@ijekavianlatin]=VCard fajl
+Name[sr@latin]=VCard fajl
+Name[sv]=VCard-fil
+Name[tr]=VCard Dosyası
+Name[uk]=Файл vCard
+Name[x-test]=xxVCard Filexx
+Name[zh_CN]=VCard 文件
+Name[zh_TW]=vCard 檔案
+Icon=text-directory
+Comment=Loads contact from vCard File
+Comment[bg]=Зареждане на контакти от файл vCard
+Comment[ca]=Carrega els contactes des d'un fitxer vCard
+Comment[ca@valencia]=Carrega els contactes des d'un fitxer vCard
+Comment[cs]=Načítá data ze souboru vizitek
+Comment[de]=Lädt Kontakte aus einer vCard-Datei
+Comment[el]=Φορτώνει επαφή από αρχείο VCard
+Comment[en_GB]=Loads contact from vCard File
+Comment[es]=Carga contactos desde un archivo vCard
+Comment[et]=Kontakti laadimine vCard-failist
+Comment[fi]=Lataa yhteystiedon vCard-tiedostosta
+Comment[fr]=Charge un contact depuis un fichier au format « vCard »
+Comment[gl]=Cargar os contactos dun ficheiro vCard
+Comment[hu]=Névjegy betöltése vCard fájlból
+Comment[it]=Carica contatti da un file vCard
+Comment[ko]=vCard 파일에서 연락처를 가져옵니다
+Comment[nb]=Laster kontakt fra  vCard-fila
+Comment[nds]=Laadt Kontakten ut en vCard-Datei
+Comment[nl]=Laadt contactpersoon uit een vCard-bestand
+Comment[pl]=Wczytuje kontakty z pliku vCard
+Comment[pt]=Carrega um contacto de um ficheiro vCard
+Comment[pt_BR]=Carrega os contatos de um arquivo vCard
+Comment[ru]=Загрузка контакта из файла vCard
+Comment[sk]=Načíta kontakt zo súboru vCard
+Comment[sl]=Naloži stik iz datoteke vCard
+Comment[sr]=Учитава контакт из в‑кард фајла
+Comment[sr@ijekavian]=Учитава контакт из в‑кард фајла
+Comment[sr@ijekavianlatin]=Učitava kontakt iz vCard fajla
+Comment[sr@latin]=Učitava kontakt iz vCard fajla
+Comment[sv]=Laddar kontakt från en vCard-fil
+Comment[tr]=VCard dosyasından bağlantı yükler
+Comment[uk]=Завантажує дані запису контакту із файла vCard
+Comment[x-test]=xxLoads contact from vCard Filexx
+Comment[zh_CN]=从 vCard 文件载入联系人
+Comment[zh_TW]=從 vCard 檔載入聯絡人
+
+[Wizard]
+Type=text/vcard
+Script=vcardwizard.es
+
+[Translate]
+Filename=accountwizard_vcard
diff --git a/resources/vcard/wizard/vcardwizard.es.cmake b/resources/vcard/wizard/vcardwizard.es.cmake
new file mode 100644 (file)
index 0000000..7f8ad3e
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+    Copyright (c) 2010 Till Adam <adam@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+var page = Dialog.addPage( "vcardwizard.ui", qsTr("Settings") );
+
+page.widget().lineEdit.text = "${VCARD_FILE_DEFAULT_PATH}";
+
+function validateInput()
+{
+  if ( page.widget().lineEdit.text == "" ) {
+    page.setValid( false );
+  } else {
+    page.setValid( true );
+  }
+}
+
+function setup()
+{
+  var vcardRes = SetupManager.createResource( "akonadi_vcard_resource" );
+  vcardRes.setOption( "Path", page.widget().lineEdit.text );
+  vcardRes.setName( qsTr("Default Contact") );
+  SetupManager.execute();
+}
+
+page.widget().lineEdit.textChanged.connect( validateInput );
+page.pageLeftNext.connect( setup );
+validateInput();
diff --git a/resources/vcard/wizard/vcardwizard.ui b/resources/vcard/wizard/vcardwizard.ui
new file mode 100644 (file)
index 0000000..18d4f30
--- /dev/null
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>icalWizard</class>
+ <widget class="QWidget" name="vcardWizard">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>300</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QLabel" name="label">
+       <property name="text">
+        <string>Filename:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QLineEdit" name="lineEdit"/>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <spacer name="verticalSpacer_2">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>138</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/resources/vcarddir/CMakeLists.txt b/resources/vcarddir/CMakeLists.txt
new file mode 100644 (file)
index 0000000..28fdd04
--- /dev/null
@@ -0,0 +1,41 @@
+add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_vcarddir_resource\")
+
+########### next target ###############
+
+set( vcarddirresource_SRCS
+  vcarddirresource.cpp
+  dirsettingsdialog.cpp
+)
+
+kconfig_add_kcfg_files(vcarddirresource_SRCS settings.kcfgc)
+kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/vcarddirresource.kcfg org.kde.Akonadi.VCardDirectory.Settings)
+qt5_add_dbus_adaptor(vcarddirresource_SRCS
+  ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.VCardDirectory.Settings.xml settings.h Settings
+)
+
+install( FILES vcarddirresource.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents" )
+
+add_executable(akonadi_vcarddir_resource ${vcarddirresource_SRCS})
+
+if( APPLE )
+  set_target_properties(akonadi_vcarddir_resource PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../Info.plist.template)
+  set_target_properties(akonadi_vcarddir_resource PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.VCardDirectory")
+  set_target_properties(akonadi_vcarddir_resource PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi VCardDirectory Resource")
+endif ()
+
+
+target_link_libraries(akonadi_vcarddir_resource
+  KF5::AkonadiCore
+  KF5::AkonadiAgentBase
+  KF5::Contacts
+  KF5::I18n
+  KF5::TextWidgets
+  KF5::KIOWidgets
+  KF5::ConfigWidgets
+  KF5::WindowSystem
+  Qt5::DBus
+)
+
+add_subdirectory(wizard)
+
+install(TARGETS akonadi_vcarddir_resource ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
diff --git a/resources/vcarddir/Messages.sh b/resources/vcarddir/Messages.sh
new file mode 100644 (file)
index 0000000..3157858
--- /dev/null
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+$EXTRACTRC `find . -name \*.ui` `find . -name \*.kcfg` >> rc.cpp || exit 11
+$XGETTEXT *.cpp -o $podir/akonadi_vcarddir_resource.pot
diff --git a/resources/vcarddir/dirsettingsdialog.cpp b/resources/vcarddir/dirsettingsdialog.cpp
new file mode 100644 (file)
index 0000000..b75413e
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "dirsettingsdialog.h"
+
+#include "settings.h"
+
+#include <KConfigDialogManager>
+#include <KWindowSystem>
+#include <KLocalizedString>
+#include <QUrl>
+
+#include <QTimer>
+#include <QDialogButtonBox>
+#include <QPushButton>
+#include <QVBoxLayout>
+
+using namespace Akonadi;
+
+SettingsDialog::SettingsDialog(WId windowId)
+    : QDialog()
+{
+    QWidget *mainWidget = new QWidget(this);
+    QVBoxLayout *mainLayout = new QVBoxLayout;
+    setLayout(mainLayout);
+    mainLayout->addWidget(mainWidget);
+    ui.setupUi(mainWidget);
+    ui.kcfg_Path->setMode(KFile::LocalOnly | KFile::Directory);
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    mOkButton = buttonBox->button(QDialogButtonBox::Ok);
+    mOkButton->setDefault(true);
+    mOkButton->setShortcut(Qt::CTRL | Qt::Key_Return);
+    connect(buttonBox, &QDialogButtonBox::accepted, this, &SettingsDialog::accept);
+    connect(buttonBox, &QDialogButtonBox::rejected, this, &SettingsDialog::reject);
+    mainLayout->addWidget(buttonBox);
+
+    if (windowId) {
+        KWindowSystem::setMainWindow(this, windowId);
+    }
+
+    connect(mOkButton, &QPushButton::clicked, this, &SettingsDialog::save);
+
+    connect(ui.kcfg_Path, &KUrlRequester::textChanged, this, &SettingsDialog::validate);
+    connect(ui.kcfg_ReadOnly, &QCheckBox::toggled, this, &SettingsDialog::validate);
+
+    QTimer::singleShot(0, this, &SettingsDialog::validate);
+
+    ui.kcfg_Path->setUrl(QUrl::fromLocalFile(Settings::self()->path()));
+    ui.kcfg_AutosaveInterval->setSuffix(ki18np(" minute", " minutes"));
+    mManager = new KConfigDialogManager(this, Settings::self());
+    mManager->updateWidgets();
+}
+
+void SettingsDialog::save()
+{
+    mManager->updateSettings();
+    Settings::self()->setPath(ui.kcfg_Path->url().toLocalFile());
+    Settings::self()->save();
+}
+
+void SettingsDialog::validate()
+{
+    const QUrl currentUrl = ui.kcfg_Path->url();
+    if (currentUrl.isEmpty()) {
+        mOkButton->setEnabled(false);
+        return;
+    }
+
+    const QFileInfo file(currentUrl.toLocalFile());
+    if (file.exists() && !file.isWritable()) {
+        ui.kcfg_ReadOnly->setEnabled(false);
+        ui.kcfg_ReadOnly->setChecked(true);
+    } else {
+        ui.kcfg_ReadOnly->setEnabled(true);
+    }
+    mOkButton->setEnabled(true);
+}
diff --git a/resources/vcarddir/dirsettingsdialog.h b/resources/vcarddir/dirsettingsdialog.h
new file mode 100644 (file)
index 0000000..c56f495
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+    Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef DIRSETTINGSDIALOG_H
+#define DIRSETTINGSDIALOG_H
+
+#include "ui_settingsdialog.h"
+
+#include <QDialog>
+#include <QPushButton>
+
+class KConfigDialogManager;
+
+namespace Akonadi
+{
+
+class SettingsDialog : public QDialog
+{
+    Q_OBJECT
+public:
+    explicit SettingsDialog(WId windowId);
+
+private Q_SLOTS:
+    void save();
+    void validate();
+
+private:
+    Ui::SettingsDialog ui;
+    KConfigDialogManager *mManager;
+    QPushButton *mOkButton;
+};
+
+}
+
+#endif
diff --git a/resources/vcarddir/settings.kcfgc b/resources/vcarddir/settings.kcfgc
new file mode 100644 (file)
index 0000000..dd4cb03
--- /dev/null
@@ -0,0 +1,7 @@
+File=vcarddirresource.kcfg
+ClassName=Settings
+Mutators=true
+ItemAccessors=true
+SetUserTexts=true
+Singleton=true
+GlobalEnums=true
diff --git a/resources/vcarddir/vcarddirresource.cpp b/resources/vcarddir/vcarddirresource.cpp
new file mode 100644 (file)
index 0000000..b8c5faf
--- /dev/null
@@ -0,0 +1,287 @@
+/*
+    Copyright (c) 2008 Tobias Koenig <tokoe@kde.org>
+    Copyright (c) 2008 Bertjan Broeksema <broeksema@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#include "vcarddirresource.h"
+
+#include "settingsadaptor.h"
+#include "dirsettingsdialog.h"
+
+#include <QtCore/QDir>
+#include <QtCore/QDirIterator>
+#include <QtCore/QFile>
+
+#include <changerecorder.h>
+#include <entitydisplayattribute.h>
+#include <itemfetchscope.h>
+
+#include <QIcon>
+#include <KLocalizedString>
+#include <QDebug>
+
+using namespace Akonadi;
+
+VCardDirResource::VCardDirResource(const QString &id)
+    : ResourceBase(id)
+{
+    // setup the resource
+    new SettingsAdaptor(Settings::self());
+    QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"),
+            Settings::self(), QDBusConnection::ExportAdaptors);
+
+    changeRecorder()->itemFetchScope().fetchFullPayload();
+}
+
+VCardDirResource::~VCardDirResource()
+{
+    // clear cache
+    mAddressees.clear();
+}
+
+void VCardDirResource::aboutToQuit()
+{
+    Settings::self()->save();
+}
+
+void VCardDirResource::configure(WId windowId)
+{
+    SettingsDialog dlg(windowId);
+    dlg.setWindowIcon(QIcon::fromTheme(QStringLiteral("text-directory")));
+    if (dlg.exec()) {
+        initializeVCardDirectory();
+        loadAddressees();
+
+        synchronize();
+
+        Q_EMIT configurationDialogAccepted();
+    } else {
+        Q_EMIT configurationDialogRejected();
+    }
+}
+
+bool VCardDirResource::loadAddressees()
+{
+    mAddressees.clear();
+
+    QDirIterator it(vCardDirectoryName());
+    while (it.hasNext()) {
+        it.next();
+        if (it.fileName() != QLatin1String(".") && it.fileName() != QLatin1String("..") && it.fileName() != QLatin1String("WARNING_README.txt")) {
+            QFile file(it.filePath());
+            if (file.open(QIODevice::ReadOnly)) {
+                const QByteArray data = file.readAll();
+                file.close();
+
+                const KContacts::Addressee addr = mConverter.parseVCard(data);
+                if (!addr.isEmpty()) {
+                    mAddressees.insert(addr.uid(), addr);
+                }
+            } else {
+                qDebug() << " file can't be load " << it.filePath();
+            }
+        }
+    }
+
+    Q_EMIT status(Idle);
+
+    return true;
+}
+
+bool VCardDirResource::retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &)
+{
+    const QString remoteId = item.remoteId();
+    if (!mAddressees.contains(remoteId)) {
+        Q_EMIT error(i18n("Contact with uid '%1' not found.", remoteId));
+        return false;
+    }
+
+    Item newItem(item);
+    newItem.setPayload<KContacts::Addressee>(mAddressees.value(remoteId));
+    itemRetrieved(newItem);
+
+    return true;
+}
+
+void VCardDirResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &)
+{
+    if (Settings::self()->readOnly()) {
+        Q_EMIT error(i18n("Trying to write to a read-only directory: '%1'", vCardDirectoryName()));
+        cancelTask();
+        return;
+    }
+
+    KContacts::Addressee addressee;
+    if (item.hasPayload<KContacts::Addressee>()) {
+        addressee  = item.payload<KContacts::Addressee>();
+    }
+
+    if (!addressee.isEmpty()) {
+        // add it to the cache...
+        mAddressees.insert(addressee.uid(), addressee);
+
+        // ... and write it through to the file system
+        const QByteArray data = mConverter.createVCard(addressee);
+
+        QFile file(vCardDirectoryFileName(addressee.uid()));
+        file.open(QIODevice::WriteOnly);
+        file.write(data);
+        file.close();
+
+        // report everything ok
+        Item newItem(item);
+        newItem.setRemoteId(addressee.uid());
+        changeCommitted(newItem);
+
+    } else {
+        changeProcessed();
+    }
+}
+
+void VCardDirResource::itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &)
+{
+    if (Settings::self()->readOnly()) {
+        Q_EMIT error(i18n("Trying to write to a read-only directory: '%1'", vCardDirectoryName()));
+        cancelTask();
+        return;
+    }
+
+    KContacts::Addressee addressee;
+    if (item.hasPayload<KContacts::Addressee>()) {
+        addressee  = item.payload<KContacts::Addressee>();
+    }
+
+    if (!addressee.isEmpty()) {
+        // change it in the cache...
+        mAddressees.insert(addressee.uid(), addressee);
+
+        // ... and write it through to the file system
+        const QByteArray data = mConverter.createVCard(addressee);
+
+        QFile file(vCardDirectoryFileName(addressee.uid()));
+        if (file.open(QIODevice::WriteOnly)) {
+            file.write(data);
+            file.close();
+
+            Item newItem(item);
+            newItem.setRemoteId(addressee.uid());
+            changeCommitted(newItem);
+        } else {
+            qDebug() << " We can't write in file " << file.fileName();
+        }
+
+    } else {
+        changeProcessed();
+    }
+}
+
+void VCardDirResource::itemRemoved(const Akonadi::Item &item)
+{
+    if (Settings::self()->readOnly()) {
+        Q_EMIT error(i18n("Trying to write to a read-only directory: '%1'", vCardDirectoryName()));
+        cancelTask();
+        return;
+    }
+
+    // remove it from the cache...
+    if (mAddressees.contains(item.remoteId())) {
+        mAddressees.remove(item.remoteId());
+    }
+
+    // ... and remove it from the file system
+    QFile::remove(vCardDirectoryFileName(item.remoteId()));
+
+    changeProcessed();
+}
+
+void VCardDirResource::retrieveCollections()
+{
+    Collection c;
+    c.setParentCollection(Collection::root());
+    c.setRemoteId(vCardDirectoryName());
+    c.setName(name());
+    QStringList mimeTypes;
+    mimeTypes << KContacts::Addressee::mimeType();
+    c.setContentMimeTypes(mimeTypes);
+    if (Settings::self()->readOnly()) {
+        c.setRights(Collection::ReadOnly);
+    } else {
+        Collection::Rights rights;
+        rights |= Collection::CanChangeItem;
+        rights |= Collection::CanCreateItem;
+        rights |= Collection::CanDeleteItem;
+        rights |= Collection::CanChangeCollection;
+        c.setRights(rights);
+    }
+
+    EntityDisplayAttribute *attr = c.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
+    attr->setDisplayName(i18n("Contacts Folder"));
+    attr->setIconName(QStringLiteral("x-office-address-book"));
+
+    Collection::List list;
+    list << c;
+    collectionsRetrieved(list);
+}
+
+void VCardDirResource::retrieveItems(const Akonadi::Collection &)
+{
+    Item::List items;
+    items.reserve(mAddressees.count());
+
+    foreach (const KContacts::Addressee &addressee, mAddressees) {
+        Item item;
+        item.setRemoteId(addressee.uid());
+        item.setMimeType(KContacts::Addressee::mimeType());
+        items.append(item);
+    }
+
+    itemsRetrieved(items);
+}
+
+QString VCardDirResource::vCardDirectoryName() const
+{
+    return Settings::self()->path();
+}
+
+QString VCardDirResource::vCardDirectoryFileName(const QString &file) const
+{
+    return Settings::self()->path() + QDir::separator() + file;
+}
+
+void VCardDirResource::initializeVCardDirectory() const
+{
+    QDir dir(vCardDirectoryName());
+
+    // if folder does not exists, create it
+    if (!dir.exists()) {
+        QDir::root().mkpath(dir.absolutePath());
+    }
+
+    // check whether warning file is in place...
+    QFile file(dir.absolutePath() + QDir::separator() + QLatin1String("WARNING_README.txt"));
+    if (!file.exists()) {
+        // ... if not, create it
+        file.open(QIODevice::WriteOnly);
+        file.write("Important Warning!!!\n\n"
+                   "Don't create or copy vCards inside this folder manually, they are managed by the Akonadi framework!\n");
+        file.close();
+    }
+}
+
+AKONADI_RESOURCE_MAIN(VCardDirResource)
+
diff --git a/resources/vcarddir/vcarddirresource.desktop b/resources/vcarddir/vcarddirresource.desktop
new file mode 100644 (file)
index 0000000..181925d
--- /dev/null
@@ -0,0 +1,90 @@
+[Desktop Entry]
+Name=vCard Directory
+Name[bg]=Папка с vCard
+Name[bs]=VCard direktorij
+Name[ca]=Directori vCard
+Name[ca@valencia]=Directori vCard
+Name[cs]=Adresář vizitek
+Name[da]=vCard-mappe
+Name[de]=vCard-Ordner
+Name[el]=VCard Κατάλογος
+Name[en_GB]=vCard Directory
+Name[es]=Directorio vCard
+Name[et]=vCardide kataloog
+Name[fi]=vCard-kansio
+Name[fr]=Dossier « vCard »
+Name[ga]=Comhadlann v-Chártaí
+Name[gl]=Directorio de vCard
+Name[hu]=vCard mappa
+Name[ia]=Directorio de VCard
+Name[it]=Directory vCard
+Name[kk]=VCard қапшығы
+Name[ko]=vCard 디렉터리
+Name[lt]=vCard katalogas
+Name[nb]=vCard-mappe
+Name[nds]=VCard-Orner
+Name[nl]=vCard-map
+Name[pl]=Katalog vCard
+Name[pt]=Pasta de vCard's
+Name[pt_BR]=Pasta de VCards
+Name[ru]=Каталог с файлами vCard
+Name[sk]=Adresár VCard
+Name[sl]=Mapa vCard
+Name[sr]=В‑кард фасцикла
+Name[sr@ijekavian]=В‑кард фасцикла
+Name[sr@ijekavianlatin]=VCard fascikla
+Name[sr@latin]=VCard fascikla
+Name[sv]=vCard-katalog
+Name[tr]=VCard Dizini
+Name[uk]=Каталог vCard
+Name[x-test]=xxvCard Directoryxx
+Name[zh_CN]=vCard 目录
+Name[zh_TW]=vCard 目錄
+Comment=Loads data from a directory with vCards
+Comment[bg]=Зареждане на данни от папка с vCard
+Comment[bs]=Učitava podakte iz direktorija sa VCards
+Comment[ca]=Carrega les dades des d'un directori amb vCard
+Comment[ca@valencia]=Carrega les dades des d'un directori amb vCard
+Comment[cs]=Načítá data z adresáře vizitek
+Comment[da]=Indlæser data fra en mappe med vCards
+Comment[de]=Daten werden aus einem Ordner mit vCard-Dateien geladen
+Comment[el]=Φορτώνει δεδομένα από έναν κατάλογο που περιέχει κάρτες VCard
+Comment[en_GB]=Loads data from a directory with vCards
+Comment[es]=Carga datos desde un directorio con archivos vCard
+Comment[et]=Andmete laadimine vCardide kataloogist
+Comment[fi]=Lataa tietoa vCardeja sisältävästä kansiosta
+Comment[fr]=Charge des données depuis un dossier contenant des fichiers « vCard »
+Comment[ga]=Luchtaíonn sé seo sonraí ó chomhadlann ina bhfuil v-Chártaí
+Comment[gl]=Carga datos desde un cartafol con vCards
+Comment[hu]=Adatokat tölt be egy vCardokat tartalmazó mappából
+Comment[ia]=Carga datos ex un directorio con vCards
+Comment[it]=Carica dati da una directory con dei file vCard
+Comment[kk]=VCard қапшығынан деректі алып береді
+Comment[ko]=vCard 디렉터리에서 데이터를 가져옵니다
+Comment[lt]=įkelia duomenis iš katalogo su VCard failais
+Comment[nb]=Laster data fra en mappe med vCard-filer
+Comment[nds]=Laadt Daten ut en Orner mit VCard-Dateien
+Comment[nl]=Laadt gegevens van een map met vCards
+Comment[pl]=Wczytuje dane z katalogu wizytówkami vCard
+Comment[pt]=Carrega os dados de uma pasta com ficheiros vCard
+Comment[pt_BR]=Carrega os dados de uma pasta com vCards
+Comment[ru]=Загрузка данных из папки с файлами vCard
+Comment[sk]=Načíta dáta z adresára s vCards
+Comment[sl]=Naloži podatke iz mape z datotekami vCard
+Comment[sr]=Учитава податке из фасцикле са в‑кардовима
+Comment[sr@ijekavian]=Учитава податке из фасцикле са в‑кардовима
+Comment[sr@ijekavianlatin]=Učitava podatke iz fascikle sa vCardovima
+Comment[sr@latin]=Učitava podatke iz fascikle sa vCardovima
+Comment[sv]=Läser in data från en katalog med vCard-filer
+Comment[tr]=VCard dosyaları bulunan bir dizinden verileri yükler
+Comment[uk]=Завантажує дані з каталогу з vCard
+Comment[x-test]=xxLoads data from a directory with vCardsxx
+Comment[zh_CN]=从包含 vCard 文件的目录载入数据
+Comment[zh_TW]=從含有 vCard 檔的目錄載入資料
+Type=AkonadiResource
+Exec=akonadi_vcarddir_resource
+Icon=text-directory
+
+X-Akonadi-MimeTypes=text/directory
+X-Akonadi-Capabilities=Resource
+X-Akonadi-Identifier=akonadi_vcarddir_resource
diff --git a/resources/vcarddir/vcarddirresource.h b/resources/vcarddir/vcarddirresource.h
new file mode 100644 (file)
index 0000000..95657c1
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+    Copyright (c) 2008 Tobias Koenig <tokoe@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+#ifndef VCARDDIRRESOURCE_H
+#define VCARDDIRRESOURCE_H
+
+#include <resourcebase.h>
+
+#include <kcontacts/addressee.h>
+#include <kcontacts/vcardconverter.h>
+
+class VCardDirResource : public Akonadi::ResourceBase, public Akonadi::AgentBase::Observer
+{
+    Q_OBJECT
+
+public:
+    VCardDirResource(const QString &id);
+    ~VCardDirResource();
+
+public Q_SLOTS:
+    void configure(WId windowId) Q_DECL_OVERRIDE;
+    void aboutToQuit() Q_DECL_OVERRIDE;
+
+protected Q_SLOTS:
+    void retrieveCollections() Q_DECL_OVERRIDE;
+    void retrieveItems(const Akonadi::Collection &col) Q_DECL_OVERRIDE;
+    bool retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+
+protected:
+    void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) Q_DECL_OVERRIDE;
+    void itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &parts) Q_DECL_OVERRIDE;
+    void itemRemoved(const Akonadi::Item &item) Q_DECL_OVERRIDE;
+
+private:
+    bool loadAddressees();
+    QString vCardDirectoryName() const;
+    QString vCardDirectoryFileName(const QString &file) const;
+    void initializeVCardDirectory() const;
+
+private:
+    QMap<QString, KContacts::Addressee> mAddressees;
+    KContacts::VCardConverter mConverter;
+};
+
+#endif
diff --git a/resources/vcarddir/vcarddirresource.kcfg b/resources/vcarddir/vcarddirresource.kcfg
new file mode 100644 (file)
index 0000000..426e00d
--- /dev/null
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:kcfg="http://www.kde.org/standards/kcfg/1.0"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
+      http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
+  <kcfgfile/>
+  <group name="General">
+    <entry name="Path" type="Path">
+      <label>Path to vCard directory</label>
+      <default></default>
+    </entry>
+    <entry name="AutosaveInterval" type="UInt">
+      <label>Autosave interval time (in minutes).</label>
+      <default>5</default>
+    </entry>
+    <entry name="ReadOnly" type="Bool">
+      <label>Do not change the actual backend data.</label>
+      <default>false</default>
+    </entry>
+  </group>
+</kcfg>
diff --git a/resources/vcarddir/wizard/CMakeLists.txt b/resources/vcarddir/wizard/CMakeLists.txt
new file mode 100644 (file)
index 0000000..292b586
--- /dev/null
@@ -0,0 +1,4 @@
+set(VCARDDIR_FILE_DEFAULT_PATH "$HOME/")
+
+configure_file(vcarddirwizard.es.cmake ${CMAKE_CURRENT_BINARY_DIR}/vcarddirwizard.es)
+install ( FILES vcarddirwizard.desktop ${CMAKE_CURRENT_BINARY_DIR}/vcarddirwizard.es vcarddirwizard.ui DESTINATION ${KDE_INSTALL_DATADIR}/akonadi/accountwizard/vcarddir )
diff --git a/resources/vcarddir/wizard/Messages.sh b/resources/vcarddir/wizard/Messages.sh
new file mode 100644 (file)
index 0000000..4622ba0
--- /dev/null
@@ -0,0 +1,4 @@
+#! /usr/bin/env bash
+$EXTRACTRC *.ui >> rc.cpp
+$XGETTEXT *.cpp -o $podir/accountwizard_vcarddir.pot
+$XGETTEXT -kqsTr *.es.cmake -j -o $podir/accountwizard_vcarddir.pot
diff --git a/resources/vcarddir/wizard/vcarddirwizard.desktop b/resources/vcarddir/wizard/vcarddirwizard.desktop
new file mode 100644 (file)
index 0000000..39a2a11
--- /dev/null
@@ -0,0 +1,83 @@
+[Desktop Entry]
+Name=vCard Directory
+Name[bg]=Папка с vCard
+Name[bs]=VCard direktorij
+Name[ca]=Directori vCard
+Name[ca@valencia]=Directori vCard
+Name[cs]=Adresář vizitek
+Name[da]=vCard-mappe
+Name[de]=vCard-Ordner
+Name[el]=VCard Κατάλογος
+Name[en_GB]=vCard Directory
+Name[es]=Directorio vCard
+Name[et]=vCardide kataloog
+Name[fi]=vCard-kansio
+Name[fr]=Dossier « vCard »
+Name[ga]=Comhadlann v-Chártaí
+Name[gl]=Directorio de vCard
+Name[hu]=vCard mappa
+Name[ia]=Directorio de VCard
+Name[it]=Directory vCard
+Name[kk]=VCard қапшығы
+Name[ko]=vCard 디렉터리
+Name[lt]=vCard katalogas
+Name[nb]=vCard-mappe
+Name[nds]=VCard-Orner
+Name[nl]=vCard-map
+Name[pl]=Katalog vCard
+Name[pt]=Pasta de vCard's
+Name[pt_BR]=Pasta de VCards
+Name[ru]=Каталог с файлами vCard
+Name[sk]=Adresár VCard
+Name[sl]=Mapa vCard
+Name[sr]=В‑кард фасцикла
+Name[sr@ijekavian]=В‑кард фасцикла
+Name[sr@ijekavianlatin]=VCard fascikla
+Name[sr@latin]=VCard fascikla
+Name[sv]=vCard-katalog
+Name[tr]=VCard Dizini
+Name[uk]=Каталог vCard
+Name[x-test]=xxvCard Directoryxx
+Name[zh_CN]=vCard 目录
+Name[zh_TW]=vCard 目錄
+Icon=text-directory
+Comment=Loads contact from VCard Directory
+Comment[bg]=Зареждане на контакти от папка с vCard
+Comment[ca]=Carrega els contactes des d'un directori VCard
+Comment[ca@valencia]=Carrega els contactes des d'un directori VCard
+Comment[cs]=Načítá data z adresáře vizitek
+Comment[de]=Lädt Kontakte aus einem vCard-Ordner
+Comment[el]=Φορτώνει επαφή από κατάλογο VCard
+Comment[en_GB]=Loads contact from VCard Directory
+Comment[es]=Carga contactos desde un directorio VCard
+Comment[et]=Kontakti laadimine vCardide kataloogist
+Comment[fi]=Lataa yhteystiedon VCard-kansiosta
+Comment[fr]=Charge des données depuis un dossier « vCard »
+Comment[gl]=Carga contactos dun cartafol vCard
+Comment[it]=Carica contatti da una cartella vCard
+Comment[ko]=vCard 디렉터리에서 연락처를 가져옵니다
+Comment[nb]=Laster kontakt fra vCard-mappa
+Comment[nl]=Laadt contactpersoon uit een vCard-map
+Comment[pl]=Wczytuje kontakty z katalogu wizytówek vCard
+Comment[pt]=Carrega um contacto de uma pasta de ficheiros vCard
+Comment[pt_BR]=Carrega os contatos de um diretório vCard
+Comment[ru]=Загрузка контактов из каталога с файлами vCard
+Comment[sk]=Načíta kontakt z adresára vCard
+Comment[sl]=Naloži stik iz mape z datotekami vCard
+Comment[sr]=Учитава контакт из в‑кард фасцикле
+Comment[sr@ijekavian]=Учитава контакт из в‑кард фасцикле
+Comment[sr@ijekavianlatin]=Učitava kontakt iz vCard fascikle
+Comment[sr@latin]=Učitava kontakt iz vCard fascikle
+Comment[sv]=Läser in kontakter från en vCard-katalog
+Comment[tr]=vCard Dizininden bağlantıyı yükler
+Comment[uk]=Завантажує дані запису контакту із каталогу vCard
+Comment[x-test]=xxLoads contact from VCard Directoryxx
+Comment[zh_CN]=从 VCard 目录载入联系人
+Comment[zh_TW]=從 vCard 目錄載入聯絡人
+
+[Wizard]
+Type=text/vcard
+Script=vcarddirwizard.es
+
+[Translate]
+Filename=accountwizard_vcarddir
diff --git a/resources/vcarddir/wizard/vcarddirwizard.es.cmake b/resources/vcarddir/wizard/vcarddirwizard.es.cmake
new file mode 100644 (file)
index 0000000..927fd36
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+    Copyright (c) 2010 Till Adam <adam@kde.org>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+
+var page = Dialog.addPage( "vcarddirwizard.ui", qsTr("Settings") );
+
+page.widget().lineEdit.text = "${VCARDDIR_FILE_DEFAULT_PATH}";
+
+function validateInput()
+{
+  if ( page.widget().lineEdit.text == "" ) {
+    page.setValid( false );
+  } else {
+    page.setValid( true );
+  }
+}
+
+function setup()
+{
+  var vcardRes = SetupManager.createResource( "akonadi_vcarddir_resource" );
+  vcardRes.setOption( "Path", page.widget().lineEdit.text );
+  vcardRes.setName( qsTr("Default Contact") );
+  SetupManager.execute();
+}
+
+page.widget().lineEdit.textChanged.connect( validateInput );
+page.pageLeftNext.connect( setup );
+validateInput();
diff --git a/resources/vcarddir/wizard/vcarddirwizard.ui b/resources/vcarddir/wizard/vcarddirwizard.ui
new file mode 100644 (file)
index 0000000..f0dfc5f
--- /dev/null
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>icalWizard</class>
+ <widget class="QWidget" name="vcarddirWizard">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>300</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <widget class="QLabel" name="label">
+       <property name="text">
+        <string>Path:</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QLineEdit" name="lineEdit"/>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <spacer name="verticalSpacer_2">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>138</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>